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

Upgrade script from 11.0.4 to 12.0.0-B1

Alex Trofimov 5 лет назад
Родитель
Сommit
6837d4e28f
100 измененных файлов с 35859 добавлено и 0 удалено
  1. 30 0
      upgrade/files/11.0.4-12.0.0.B1/files/favorite.php
  2. 29 0
      upgrade/files/11.0.4-12.0.0.B1/files/form.php
  3. 293 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDol.php
  4. 755 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAccount.php
  5. 796 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAcl.php
  6. 455 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAclQuery.php
  7. 177 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolBadgesQuery.php
  8. 95 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCacheMemcache.php
  9. 168 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCategoriesQuery.php
  10. 1923 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCmts.php
  11. 627 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCmtsQuery.php
  12. 328 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolConnectionQuery.php
  13. 1303 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolDb.php
  14. 97 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolDesigns.php
  15. 76 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolEmbed.php
  16. 27 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolEmbedQuery.php
  17. 65 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFactoryObject.php
  18. 46 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFactoryObjectQuery.php
  19. 279 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFavorite.php
  20. 150 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFavoriteQuery.php
  21. 220 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFeature.php
  22. 1894 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolForm.php
  23. 44 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormNested.php
  24. 56 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormNestedGhost.php
  25. 353 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormQuery.php
  26. 587 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolGrid.php
  27. 185 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolGridConnections.php
  28. 124 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLabel.php
  29. 106 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLabelQuery.php
  30. 316 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLiveUpdates.php
  31. 31 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLiveUpdatesQuery.php
  32. 55 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationField.php
  33. 76 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldGoogle.php
  34. 105 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldNominatim.php
  35. 23 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldQuery.php
  36. 55 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMap.php
  37. 55 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapGoogleStatic.php
  38. 103 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapLeaflet.php
  39. 23 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapQuery.php
  40. 149 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogs.php
  41. 33 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsQuery.php
  42. 137 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStorageFolder.php
  43. 64 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStoragePHPLog.php
  44. 70 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStorageSTDErr.php
  45. 338 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMenu.php
  46. 127 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMenuQuery.php
  47. 1009 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMetatags.php
  48. 154 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMetatagsQuery.php
  49. 194 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolModule.php
  50. 214 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolModuleQuery.php
  51. 345 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPage.php
  52. 115 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPageQuery.php
  53. 421 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPayments.php
  54. 739 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPrivacy.php
  55. 615 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolProfile.php
  56. 395 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolProfileQuery.php
  57. 174 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPush.php
  58. 256 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolReport.php
  59. 87 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolReportQuery.php
  60. 1075 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolSearch.php
  61. 62 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolSessionQuery.php
  62. 1442 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorage.php
  63. 366 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageQuery.php
  64. 242 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3.php
  65. 29 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3v4.php
  66. 168 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3v4alt.php
  67. 3027 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTemplate.php
  68. 312 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTranscoderQuery.php
  69. 77 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTwilio.php
  70. 453 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolUploader.php
  71. 237 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolVoteReactions.php
  72. 178 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/design.inc.php
  73. 47 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCategories.js
  74. 814 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCmts.js
  75. 107 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCreatePost.js
  76. 206 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolFavorite.js
  77. 69 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolForm.js
  78. 476 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolGrid.js
  79. 97 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolLabel.js
  80. 101 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolLiveUpdates.js
  81. 176 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolMenuMoreAuto.js
  82. 103 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolNestedForm.js
  83. 59 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolPage.js
  84. 649 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolUploader.js
  85. 250 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolVoteReactions.js
  86. 1158 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/functions.js
  87. 885 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/jquery.dolPopup.js
  88. 333 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/js/jquery.webForms.js
  89. 108 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/params.inc.php
  90. 310 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/profiles.inc.php
  91. 2056 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/utils.inc.php
  92. 12 0
      upgrade/files/11.0.4-12.0.0.B1/files/inc/version.inc.php
  93. 28 0
      upgrade/files/11.0.4-12.0.0.B1/files/label.php
  94. 91 0
      upgrade/files/11.0.4-12.0.0.B1/files/member.php
  95. 25 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/files/classes/BxBaseModFilesPrivacy.php
  96. 357 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralDb.php
  97. 939 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php
  98. 646 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  99. 295 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralGridAdministration.php
  100. 28 0
      upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralMenuSubmenuMoreAuto.php

+ 30 - 0
upgrade/files/11.0.4-12.0.0.B1/files/favorite.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+bx_import('BxDolLanguages');
+
+check_logged();
+
+$sSys = isset($_REQUEST['sys']) ? bx_process_input($_REQUEST['sys']) : false;
+$iObjectId = isset($_REQUEST['object_id']) ? bx_process_input($_REQUEST['object_id'], BX_DATA_INT) : 0;
+$sAction = isset($_REQUEST['action']) && preg_match ('/^[A-Za-z_-]+$/', $_REQUEST['action']) ? bx_process_input($_REQUEST['action']) : '';
+
+$oFavorite = BxDolFavorite::getObjectInstance($sSys, $iObjectId, true);
+
+if ($oFavorite && $sSys && $sAction) {
+    header('Content-Type: text/html; charset=utf-8');
+    $sMethod = 'action' . ucfirst($sAction);
+    if(method_exists($oFavorite, $sMethod))
+        echo $oFavorite->$sMethod();
+}
+
+/** @} */

+ 29 - 0
upgrade/files/11.0.4-12.0.0.B1/files/form.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+require_once(BX_DIRECTORY_PATH_INC . "utils.inc.php");
+
+bx_import('BxDolLanguages');
+
+$sObject = bx_process_input(bx_get('o'));
+$sDisplay = bx_process_input(bx_get('d'));
+$sAction = bx_process_input(bx_get('a'));
+if(!empty($sAction))
+    $sAction = 'performAction' . bx_gen_method_name($sAction);
+
+// try to create form object and call its method
+if(!empty($sObject) && !empty($sDisplay) && !empty($sAction)) {
+    $oForm = BxTemplFormView::getObjectInstance($sObject, $sDisplay);
+    if($oForm && method_exists($oForm, $sAction))
+        $oForm->$sAction();
+}
+
+/** @} */

+ 293 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDol.php

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

+ 755 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,755 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+class BxDolAccount extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_iAccountID;
+    protected $_aInfo;
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    protected function __construct ($iAccountId)
+    {
+        $iAccountId = (int)$iAccountId;
+        $sClass = get_class($this) . '_' . $iAccountId;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_iAccountID = $iAccountId; // since constructor is protected $iAccountId is always valid
+        $this->_oQuery = BxDolAccountQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this) . '_' . $this->_iProfileID;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance($mixedAccountId = false,  $bClearCache = false)
+    {
+        if (!$mixedAccountId)
+            $mixedAccountId = getLoggedId();
+
+        $iAccountId = self::getID($mixedAccountId);
+        if (!$iAccountId)
+            return false;
+
+        $sClass = __CLASS__ . '_' . $iAccountId;
+        
+        if ($bClearCache && isset($GLOBALS['bxDolClasses'][$sClass]))
+            unset($GLOBALS['bxDolClasses'][$sClass]);
+        
+        if(!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new BxDolAccount($iAccountId);
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    /**
+     * Get studio operator account singleton instance on the class
+     */
+    public static function getInstanceStudioOperator()
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+        if (!($iId = $oQuery->getStudioOperatorId()))
+            return false;
+
+        return self::getInstance($iId);
+    }
+
+    /**
+     * Get account id
+     */
+    public function id()
+    {
+        $a = $this->getInfo($this->_iAccountID);
+        return isset($a['id']) ? $a['id'] : false;
+    }
+
+    /**
+     * Check if account is confirmed, it is checked by email confirmation
+     */
+    public function isConfirmed($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $a = $this->getInfo($iId);
+
+        $bResult = false;
+        $sConfirmationType = getParam('sys_account_confirmation_type');
+        switch($sConfirmationType) {
+            case BX_ACCOUNT_CONFIRMATION_NONE:
+                $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_EMAIL:
+                if($a['email_confirmed'])
+                    $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_PHONE:
+                if($a['phone_confirmed'])
+                    $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE:
+                if($a['email_confirmed'] && $a['phone_confirmed'])
+                    $bResult = true;
+                break;
+        }
+
+        bx_alert('account', 'is_confirmed', $iId, false, array('type' => $sConfirmationType, 'override_result' => &$bResult));
+
+        return $bResult;
+    }
+    
+    public function getCurrentConfirmationStatusValue($iAccountId = false)
+    {
+        $a = $this->getInfo((int)$iAccountId);
+        $sTmp = $a['email_confirmed'] . $a['phone_confirmed'];
+        switch ($sTmp) {
+            case '01':
+                return BX_ACCOUNT_CONFIRMATION_PHONE;
+            case '10':
+                return BX_ACCOUNT_CONFIRMATION_EMAIL;
+            case '11':
+                return BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE;
+        }
+        return BX_ACCOUNT_CONFIRMATION_NONE;
+    }
+
+    public function isConfirmedEmail($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $bResult = false;
+        if(self::isNeedConfirmEmail()) {
+            $a = $this->getInfo($iId);
+
+            $bResult = $a['email_confirmed'] ? true : false;
+        }
+        else 
+            $bResult = true;
+
+        bx_alert('account', 'is_confirmed_email', $iId, false, array('override_result' => &$bResult));
+
+        return $bResult;
+    }
+
+    public function isConfirmedPhone($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $bResult = false;        
+        if(self::isNeedConfirmPhone()) {
+            $a = $this->getInfo((int)$iAccountId);
+
+            $bResult = $a['phone_confirmed'] ? true : false;
+        }
+        else
+            $bResult = true;
+
+        bx_alert('account', 'is_confirmed_phone', $iId, false, array('override_result' => &$bResult));
+
+        return $bResult;
+    }
+
+    static public function isNeedConfirmEmail()
+    {
+        if (getParam('sys_account_confirmation_type') == BX_ACCOUNT_CONFIRMATION_EMAIL || getParam('sys_account_confirmation_type') == BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE) 
+            return true;
+        return false;
+    }
+    
+    static public function isNeedConfirmPhone()
+    {
+        if (getParam('sys_account_confirmation_type') == BX_ACCOUNT_CONFIRMATION_PHONE || getParam('sys_account_confirmation_type') == BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE) 
+            return true;
+        return false;
+    }
+    
+    public function isLocked($iAccountId = false)
+    {
+        $a = $this->getInfo((int)$iAccountId);
+        return $a['locked'] ? 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 && self::isNeedConfirmEmail()) // if email_confirmation procedure is enabled - send email confirmation letter
+            $this->sendConfirmationEmail($iId);
+
+        if ($this->_oQuery->updateEmailConfirmed($isConfirmed, $iId)) {
+            $this->_aInfo = false;
+            bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * Set account phone to confirmed or unconfirmed
+     * @param  int  $isConfirmed - false: mark phone as unconfirmed, true: as confirmed
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePhoneConfirmed($isConfirmed, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        
+        if ($this->_oQuery->updatePhoneConfirmed($isConfirmed, $iId)) {
+            $this->_aInfo = false;
+            bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Change account password
+     * @param  string  $sPassword - new password
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePassword($sPassword, $iAccountId = false)
+    {
+        $sSalt = genRndSalt();
+        $sPasswordHash = encryptUserPwd($sPassword, $sSalt);
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        if((int)BxDolAccountQuery::getInstance()->updatePassword($sPasswordHash, $sSalt, $iId) > 0) {
+            bx_alert('account', 'edited', $iId, BxDolAccount::getInstance()->id(), array('action' => 'reset_password'));
+            $this->doAudit($iId, '_sys_audit_action_account_reset_password');
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Set account phone
+     * @param  string  $sPhone - phone number
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePhone($sPhone, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        if ($this->_oQuery->updatePhone($sPhone, $iId)) {
+            bx_alert('account', 'set_phone', $iId);
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Switch context automatically to the first available profile
+     * @param $iProfileIdFilterOut profile ID to exclude from the list of possible profiles
+     * @param $iAccountId account ID to use istead of current account
+     * @return true on success or false on error
+     */ 
+    public function updateProfileContextAuto($iProfileIdFilterOut = false, $iAccountId = false)
+    {
+        $oAccount = (!$iAccountId || $iAccountId == $this->_iAccountID ? $this : BxDolAccount::getInstance ($iAccountId));
+        if (!$oAccount)
+            return false;
+        $aAccountInfo = $oAccount->getInfo();
+        $aProfiles = $oAccount->getProfiles();
+        $oProfileAccount = BxDolProfile::getInstanceAccountProfile($oAccount->id());
+
+        // unset deleted profile and account profile
+        if ($iProfileIdFilterOut)
+            unset($aProfiles[$iProfileIdFilterOut]);
+        unset($aProfiles[$oProfileAccount->id()]);
+
+        if ($aProfiles) {
+            // try to use another profile
+            reset($aProfiles);
+            $iProfileId = key($aProfiles);
+        } 
+        else {
+            // if no profiles exist, use account profile
+            $iProfileId = $oProfileAccount->id();
+        }
+
+        return $oAccount->updateProfileContext($iProfileId);
+    }
+    
+    public function updateProfileContext($iSwitchToProfileId, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aInfo = $this->getInfo((int)$iId);
+        if (!$aInfo)
+            return false;
+
+        $ret = null;
+        bx_alert('account', 'before_switch_context', $iId, $iSwitchToProfileId, array('profile_id_current' => $aInfo['profile_id'], 'override_result' => &$ret));
+        if ($ret !== null)
+            return $ret;
+
+        if (!$this->_oQuery->updateCurrentProfile($iId, $iSwitchToProfileId))
+            return false;
+
+        $this->_aInfo = false;
+            
+        bx_alert('account', 'switch_context', $iId, $iSwitchToProfileId, array('profile_id_old' => $aInfo['profile_id']));
+
+        return true;
+    }
+
+    /**
+     * Send "confirmation" email
+     */
+    public function sendConfirmationEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId;
+        if(!$iAccountId)
+            $iAccountId = $this->_iAccountID;
+
+        $aAccountInfo = $this->getInfo($iAccountId);
+        if(empty($aAccountInfo) || !is_array($aAccountInfo))
+            return false;
+
+        $oKey = BxDolKey::getInstance();
+        $sConfirmationCode = $oKey->getNewKey(array('account_id' => $iAccountId));
+        $sConfirmationLink = bx_append_url_params(BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email'), array('code' => urlencode($sConfirmationCode)));
+
+        $aReplaceVars = array(
+            'name' => $this->getDisplayName($iAccountId),
+            'email' => $aAccountInfo['email'],
+            'conf_code' => $sConfirmationCode,
+            'conf_link' => $sConfirmationLink,
+            'conf_form_link' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email')
+        );
+
+        $bResult = sendMailTemplate('t_Confirmation', $iAccountId, (int)$aAccountInfo['profile_id'], $aReplaceVars, BX_EMAIL_SYSTEM);
+        if($bResult)
+            $this->doAudit($iAccountId, '_sys_audit_action_account_resend_confirmation_email');
+
+        return $bResult;
+    }
+    
+    public function sendResetPasswordEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId;
+        if(!$iAccountId)
+            $iAccountId = $this->_iAccountID;
+
+        $aAccountInfo = $this->getInfo($iAccountId);
+        if(empty($aAccountInfo) || !is_array($aAccountInfo))
+            return false;
+
+        $sKey = bx_get_reset_password_key($aAccountInfo['email']);
+
+        $aReplaceVars = array(
+            'name' => $this->getDisplayName($iAccountId),
+            'email' => $aAccountInfo['email'],
+            'key' => $sKey,
+            'forgot_password_url' => bx_get_reset_password_link_by_key($sKey)
+        );
+
+        return sendMailTemplate('t_Forgot', $iAccountId, (int)$aAccountInfo['profile_id'], $aReplaceVars, BX_EMAIL_SYSTEM);
+    }
+
+    /**
+     * Get account info
+     */
+    public function getInfo($iAccountId = false)
+    {        
+        if ($iAccountId && $iAccountId != $this->_iAccountID)
+            return $this->_oQuery->getInfoById((int)$iAccountId ? (int)$iAccountId : $this->_iAccountID);
+
+        if ($this->_aInfo)
+            return $this->_aInfo;
+
+        $this->_aInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        return $this->_aInfo;
+    }
+
+    /**
+     * Get account display name
+     */
+    public function getDisplayName($iAccountId = false)
+    {
+        $aInfo = $this->getInfo($iAccountId);
+
+        $sDisplayName = !empty($aInfo['name']) ? $aInfo['name'] : _t('_sys_txt_user_n', $aInfo['id']);
+        bx_alert('account', 'account_name', $iAccountId, 0, array('info' => $aInfo, 'display_name' => &$sDisplayName));
+
+        return bx_process_output($sDisplayName);
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUrl($iAccountId = false)
+    {
+        return 'javascript:void(0);';
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUnit($iAccountId = false, $aParams = array())
+    {
+        
+        $sTemplate = 'unit';
+        $sTemplateSize = false;
+        $aTemplateVars = array();
+        if(!empty($aParams['template'])) {
+            if(is_string($aParams['template']))
+                $sTemplate = $aParams['template'];
+            else if(is_array($aParams['template'])) {
+                if(!empty($aParams['template']['name']))
+                    $sTemplate = $aParams['template']['name'];
+
+                if(!empty($aParams['template']['size']))
+                    $sTemplateSize = $aParams['template']['size'];
+
+                if(!empty($aParams['template']['vars']))
+                    $aTemplateVars = $aParams['template']['vars'];
+            }
+        }
+        $sTemplate = 'account_' . $sTemplate . '.html';
+        if(empty($sTemplateSize))
+            $sTemplateSize = 'thumb';
+
+        $sTitle = $this->getDisplayName($iAccountId);
+
+        $aTmplVars = array(
+            'size' => $sTemplateSize,
+            'color' => implode(', ', BxDolTemplate::getColorCode(($iAccountId ? $iAccountId : $this->_iAccountID), 1.0)),
+            'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1)),
+            'content_url' => $this->getUrl($iAccountId),
+            'title' => $sTitle,
+            'title_attr' => bx_html_attribute($sTitle),
+            'bx_if:show_online' => array(
+                'condition' => $this->isOnline($iAccountId),
+                'content' => array()
+            )
+        );
+
+        if(!empty($aTemplateVars) && is_array($aTemplateVars))
+            $aTmplVars = array_merge ($aTmplVars, $aTemplateVars);
+
+        return BxDolTemplate::getInstance()->parseHtmlByName($sTemplate, $aTmplVars);
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-picture.png');
+    }
+
+    /**
+     * Get big (2x) avatar url
+     */
+    public function getAvatarBig($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-avatar-big.png');
+    }
+
+    /**
+     * Get avatar url
+     */
+    public function getAvatar($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-avatar.png');
+    }
+
+    /**
+     * Get thumb picture url
+     */
+    public function getThumb($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-thumb.png');
+    }
+
+    /**
+     * Get icon picture url
+     */
+    public function getIcon($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-icon.png');
+    }
+
+    /**
+     * Get account email
+     */
+    public function getEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aAccountInfo = $this->getInfo($iAccountId);
+        return $aAccountInfo['email'];
+    }
+
+    /**
+     * Get language ID
+     */
+    public function getLanguageId($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aAccountInfo = $this->getInfo($iAccountId);
+        return $aAccountInfo['lang_id'];
+    }
+
+    /**
+     * Is account online
+     */
+    public function isOnline($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        return $this->_oQuery->isOnline($iAccountId);
+    }
+
+    /**
+     * Validate account.
+     * @param $s - account identifier (id or email)
+     * @return account id or false if account was not found
+     */
+    static public function getID($s)
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+
+        bx_import('BxDolForm');
+        if (BxDolFormCheckerHelper::checkEmail($s)) {
+            $iId = (int)$oQuery->getIdByEmail($s);
+            return $iId ? $iId : false;
+        }
+        if (preg_match("/^\+[0-9\s]+$/", $s)) {
+            $iId = (int)$oQuery->getIdByPhone($s);
+            return $iId ? $iId : false;
+        }
+        
+        $iId = $oQuery->getIdById((int)$s);
+        return $iId ? $iId : false;
+    }
+
+    /**
+     * Check if profiles limit reached
+     */
+    public function isProfilesLimitReached ()
+    {
+        $iProfilesLimit = (int)getParam('sys_account_limit_profiles_number');
+        bx_alert('account', 'get_limit_profiles_number', 0, 0, array('account_id' => $this->_iAccountID, 'number' => &$iProfilesLimit));
+        if ($iProfilesLimit && ($iProfilesNum = $this->getProfilesNumber()) && $iProfilesNum >= $iProfilesLimit)
+            return true;
+
+        return false;
+    }
+
+    /**
+     * Get number of profiles associated with the account
+     */
+    public function getProfilesNumber ($isFilterNonSwitchableProfiles = true)
+    {
+        $a = $this->getProfilesIds($isFilterNonSwitchableProfiles);
+        return count($a);
+    }
+    
+    /**
+     * Get all profile ids associated with the account
+     */
+    public function getProfilesIds ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true)
+    {
+        $a = $this->getProfiles ($isFilterNonSwitchableProfiles, $isFilterSystemProfiles);
+        return $a ? array_keys($a) : array();
+    }
+
+    /**
+     * Get all profiles associated with the account
+     */
+    public function getProfiles ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true)
+    {
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+
+        if ($isFilterNonSwitchableProfiles) {
+            foreach ($aProfiles as $iProfileId => $aProfile) {
+                if ('system' == $aProfile['type'])
+                    continue;
+                if (!BxDolService::call($aProfile['type'], 'act_as_profile'))
+                    unset($aProfiles[$iProfileId]);
+            }
+        }
+
+        if ($isFilterSystemProfiles) {
+            foreach ($aProfiles as $iProfileId => $aProfile) {
+                if ('system' == $aProfile['type'])
+                    unset($aProfiles[$iProfileId]);
+            }
+        }
+
+        return $aProfiles;
+    }
+
+    /**
+     * Delete profile.
+     * @param $bDeleteWithContent - delete associated profiles with all its contents
+     */
+    function delete($bDeleteWithContent = false)
+    {
+        $aAccountInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        if (!$aAccountInfo)
+            return false;
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('account', 'before_delete', $this->_iAccountID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion));
+        if ($isStopDeletion)
+            return false;
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+        foreach ($aProfiles as $iProfileId => $aRow) {
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            if (!$oProfile)
+                continue;
+            $oProfile->delete(false, $bDeleteWithContent, true);
+        }
+
+        // delete profile
+        if (!$oAccountQuery->delete($this->_iAccountID))
+            return false;
+
+        // unset class instance to prevent creating the instance again
+        $sClass = __CLASS__ . '_' . $this->_iAccountID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+       // create system event
+        bx_alert('account', 'delete', $this->_iAccountID, 0, array ('delete_with_content' => $bDeleteWithContent));
+        
+        $this->doAudit($this->_iAccountID, $bDeleteWithContent ? '_sys_audit_action_account_deleted_with_content' : '_sys_audit_action_account_deleted');
+        
+        return true;
+    }
+
+    /**
+     * Add permament messages.
+     */
+    public function addInformerPermanentMessages ($oInformer)
+    {
+        if (!$this->isConfirmedEmail()) {
+            $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email') . '&resend=1';
+            $aAccountInfo = $this->getInfo();
+            $oInformer->add('sys-account-unconfirmed-email', _t('_sys_txt_account_unconfirmed_email', $sUrl, $aAccountInfo['email']), BX_INFORMER_ALERT);
+        }
+		if (!$this->isConfirmedPhone()) {
+            $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-phone') . '';
+            $aAccountInfo = $this->getInfo();
+            $oInformer->add('sys-account-unconfirmed-phone', _t('_sys_txt_account_unconfirmed_phone', $sUrl), 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);
+    }
+    
+    public function doAudit($iAccountId, $sAction, $aData = array())
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        bx_audit(
+            $iAccountId, 
+            'bx_accounts', 
+            $sAction,  
+            array('content_title' => $this->getEmail(), 'data' => $aData)
+        );
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedCreate ($iProfileId, $isPerformAction = false)
+    {
+        $aCheck = checkActionModule($iProfileId, 'create account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox($aCheck[CHECK_ACTION_MESSAGE]);
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedEdit ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile)
+            return _t('_sys_txt_access_denied');
+
+        $aProfileInfo = $oProfile->getInfo();
+        if (!$aProfileInfo || getLoggedId() != $aProfileInfo['account_id'])
+            return _t('_sys_txt_access_denied');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedDelete ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $iAccountId = (int)BxDolProfile::getInstance($iProfileId)->getAccountId();
+        if(isAdmin($iAccountId) && $iAccountId == (int)$aContentInfo['id'])
+            return _t('_sys_txt_access_denied');
+
+        $aCheck = checkActionModule($iProfileId, 'delete account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+}
+
+/** @} */

+ 796 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAcl.php

@@ -0,0 +1,796 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Message constants passed to _t_ext() function by checkAction()
+ *
+ * NOTE: checkAction() returns language dependent messages
+ */
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED',			"_sys_acl_action_not_allowed");
+define('CHECK_ACTION_MESSAGE_LIMIT_REACHED',		"_sys_acl_action_limit_reached");
+define('CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD',	"_sys_acl_action_every_period");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE',	"_sys_acl_action_not_allowed_before");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER',	"_sys_acl_action_not_allowed_after");
+
+define('CHECK_ACTION_MESSAGE_UNAUTHENTICATED',	    "_sys_acl_action_unauthenticated");
+define('CHECK_ACTION_MESSAGE_UNCONFIRMED',			"_sys_acl_action_unconfirmed");
+define('CHECK_ACTION_MESSAGE_PENDING',			    "_sys_acl_action_pending");
+define('CHECK_ACTION_MESSAGE_SUSPENDED',			"_sys_acl_action_suspended");
+
+/**
+ * Nodes of $args array that are passed to _t_ext() function by checkAction()
+ */
+define('CHECK_ACTION_LANG_FILE_ACTION', 1);
+define('CHECK_ACTION_LANG_FILE_MEMBERSHIP', 2);
+define('CHECK_ACTION_LANG_FILE_LIMIT', 3);
+define('CHECK_ACTION_LANG_FILE_PERIOD', 4);
+define('CHECK_ACTION_LANG_FILE_AFTER', 5);
+define('CHECK_ACTION_LANG_FILE_BEFORE', 6);
+define('CHECK_ACTION_LANG_FILE_SITE_EMAIL',	7);
+define('CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT', 8);
+
+/**
+ * Standard membership ID's
+ */
+define('MEMBERSHIP_ID_NON_MEMBER', 1);
+define('MEMBERSHIP_ID_ACCOUNT', 2);
+define('MEMBERSHIP_ID_STANDARD', 3);
+define('MEMBERSHIP_ID_UNCONFIRMED', 4);
+define('MEMBERSHIP_ID_PENDING', 5);
+define('MEMBERSHIP_ID_SUSPENDED', 6);
+define('MEMBERSHIP_ID_MODERATOR', 7);
+define('MEMBERSHIP_ID_ADMINISTRATOR', 8);
+
+/**
+ * Indices for checkAction() result array
+ */
+define('CHECK_ACTION_RESULT', 0);
+define('CHECK_ACTION_MESSAGE', 1);
+define('CHECK_ACTION_PARAMETER', 3);
+
+/**
+ * CHECK_ACTION_RESULT node values
+ */
+define('CHECK_ACTION_RESULT_ALLOWED', 0);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED', 1);
+define('CHECK_ACTION_RESULT_NOT_ACTIVE', 2);
+define('CHECK_ACTION_RESULT_LIMIT_REACHED', 3);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE', 4);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER', 5);
+
+/**
+ * Standard period units
+ */
+define('MEMBERSHIP_PERIOD_UNIT_DAY', 'day');
+define('MEMBERSHIP_PERIOD_UNIT_WEEK', 'week');
+define('MEMBERSHIP_PERIOD_UNIT_MONTH', 'month');
+define('MEMBERSHIP_PERIOD_UNIT_YEAR', 'year');
+
+
+class BxDolAcl extends BxDolFactory implements iBxDolSingleton
+{
+    protected static $_aCacheData = array();
+
+    protected $oDb;
+
+    protected $_aStandardMemberships = array(
+        MEMBERSHIP_ID_NON_MEMBER => 1,
+        MEMBERSHIP_ID_ACCOUNT => 1,
+        MEMBERSHIP_ID_UNCONFIRMED => 1,
+        MEMBERSHIP_ID_PENDING => 1,
+        MEMBERSHIP_ID_SUSPENDED => 1,
+        MEMBERSHIP_ID_STANDARD => 1,
+    );
+
+    protected $_aProfileStatus2LevelMap = array (
+        BX_PROFILE_STATUS_SUSPENDED => MEMBERSHIP_ID_SUSPENDED,
+        BX_PROFILE_STATUS_PENDING => MEMBERSHIP_ID_PENDING,
+    );
+
+    protected $_aLevel2MessageMap = array (
+        MEMBERSHIP_ID_NON_MEMBER => '_sys_acl_action_unauthenticated',
+        MEMBERSHIP_ID_ACCOUNT => '_sys_acl_action_account',
+        MEMBERSHIP_ID_UNCONFIRMED => '_sys_acl_action_unconfirmed',
+        MEMBERSHIP_ID_PENDING => '_sys_acl_action_pending',
+        MEMBERSHIP_ID_SUSPENDED => '_sys_acl_action_suspended',
+    );
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->oDb = BxDolAclQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplAcl();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+	/**
+     * Get necessary condition array to use membership level in search classes
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of conditions is returned
+     */
+    public function getContentByLevelAsCondition($sContentField, $mixedLevelId)
+    {
+        $iLevelId = !is_array($mixedLevelId) ? $mixedLevelId : 0;
+        if (!$iLevelId && is_array($mixedLevelId) && 1 == count($mixedLevelId)) {
+            $a = array_values($mixedLevelId);
+            $iLevelId = array_shift($a);
+        }
+
+        // unconfirmed
+        if (MEMBERSHIP_ID_UNCONFIRMED == $iLevelId) {
+            return array(
+                'restriction_sql' => ' AND `sys_accounts`.`email_confirmed` = 0 ',
+                'restriction' => array (),
+                'join' => array (),
+            );
+        }
+        // standard
+        elseif (MEMBERSHIP_ID_STANDARD == $iLevelId) {
+            return array(
+                'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `tlm`.`IDMember` IS NULL AND `sys_accounts`.`email_confirmed` != 0 ',
+                'restriction' => array (
+                ),
+                'join' => array (
+                    'acl_members' => array(
+                        'type' => 'LEFT',
+                        'table' => 'sys_acl_levels_members',
+                        'table_alias' => 'tlm',
+                        'mainField' => $sContentField,
+                        'onField' => 'IDMember',
+                        'joinFields' => array(),
+                    ),
+                ),
+            );
+        }
+        // other levels
+        else {
+            return array(
+                'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `sys_accounts`.`email_confirmed` != 0 ',
+                'restriction' => array (
+                    'acl_members' => array(
+                        'value' => $mixedLevelId,
+                        'field' => 'IDLevel',
+                        'operator' => is_array($mixedLevelId) ? 'in' : '=',
+                        'table' => 'tlm',
+                    ),
+                ),
+                'join' => array (
+                    'acl_members' => array(
+                        'type' => 'INNER',
+                        'table' => 'sys_acl_levels_members',
+                        'table_alias' => 'tlm',
+                        'mainField' => $sContentField,
+                        'onField' => 'IDMember',
+                        'joinFields' => array(),
+                    ),
+                ),
+            );
+        }
+    }
+
+	/**
+     * Get necessary parts of SQL query to use membership levels in other queries
+     * @param $sContentTable content table name
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId)
+    {
+        return $this->oDb->getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId);
+    }
+
+    /**
+     * Check if member has one of the provided membership levels
+     * @param $mixedPermissions - integer value (every bit is matched with some membership ID) or an array of membership IDs to check permissions for
+     * @param $iProfileId - profile to check, if it isn't provided or is false then currently logged in profile is used.
+     * @return true if member has privided membership levels, or false if member hasn't.
+     */
+    public function isMemberLevelInSet($mixedPermissions, $iProfileId = false)
+    {
+        $iPermissions = 0;
+        if(is_array($mixedPermissions))
+            foreach($mixedPermissions as $iPermissionId)
+                $iPermissions += pow(2, $iPermissionId - 1);
+        else if(is_numeric($mixedPermissions))
+            $iPermissions = (int)$mixedPermissions;
+
+        if(!$iPermissions)
+            return false;
+
+        return ($iPermissions & $this->getMemberLevelBit($iProfileId));
+    }
+
+    /**
+     * Get user's membership level bit for bitwise operarions
+     */
+    public function getMemberLevelBit($iProfileId = 0)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $aACL = $this->getMemberMembershipInfo($iProfileId);
+        return 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)
+     * 		$arg0[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT]	= time when new period begins, so counter will be reset
+     *
+     * 	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;
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT] = bx_time_js($iPeriodEnd);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_LIMIT_REACHED;
+                $aResult[CHECK_ACTION_MESSAGE] = '<div class="bx-acl-err-msg">' . _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, $bFilterOutSystemAutomaticLevels = false)
+    {
+        $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);
+
+        if ($bFilterOutSystemAutomaticLevels) {
+            unset($aLevels[MEMBERSHIP_ID_NON_MEMBER]);
+            unset($aLevels[MEMBERSHIP_ID_ACCOUNT]);
+            unset($aLevels[MEMBERSHIP_ID_UNCONFIRMED]);
+            unset($aLevels[MEMBERSHIP_ID_PENDING]);
+            unset($aLevels[MEMBERSHIP_ID_SUSPENDED]);
+        }
+
+        return $aLevels;
+    }
+
+    function getMembershipsBy($aParams)
+    {
+        $aLevels = array();
+        $this->oDb->getLevels($aParams, $aLevels, false);
+
+        return $aLevels;
+    }
+
+    /**
+     * Get info about a given membership
+     *
+     * @param  int    $iLevelId membership to get info about
+     * @return array(
+     *                         'id'					=>	ID,
+     *                         'name'					=>	name,
+     *                         'icon'					=>	icon,
+     *                         'description'			=>	description,
+     *                         'active'				=>	active,
+     *                         'purchasable'			=>	purchasable,
+     *                         'removable'				=>	removable
+     *                         'quota_size'			=>	quota size,
+     *                         'quota_number'			=>	quota number,
+     *                         'quota_max_file_size'	=>	quota max file size
+     *                         )
+     */
+    function getMembershipInfo($iLevelId)
+    {
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        return $aLevel;
+    }
+
+    /**
+     * Retrieves information about membership for a given profile at a given moment.
+     *
+     * If there are no memberships purchased/assigned to the
+     * given profile or all of them have expired at the given point,
+     * the profile is assumed to be a standard profile, and the function
+     * returns	information about the Standard membership. This will
+     * also happen if a profile wasnt actually registered in the database
+     * at that point - the function will still return info about Standard
+     * membership, not the Non-member one.
+     *
+     * If there is no profile with the given $iProfileId,
+     * the function returns information about the Non-member or Authenticated
+     * predefined membership.
+     *
+     * The Standard, Authenticated and Non-member memberships have their
+     * DateStarts and DateExpires attributes set to NULL.
+     *
+     * @param  int    $iProfileId ID of a profile to get info about
+     * @param  int    $time       specifies the time to use when determining membership; if not specified, the function takes the current time
+     * @return array(
+     *                           'id'			=> membership id,
+     *                           'name'			=> membership name,
+     *                           'date_starts'	=> (UNIX timestamp) date/time purchased,
+     *                           'date_expires'	=> (UNIX timestamp) date/time expires
+     *                           )
+     */
+    function getMemberMembershipInfo($iProfileId, $iTime = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateStarts = $aMembership['date_starts'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, ((int)$iDateStarts < 1 ? 0 : $iDateStarts - 1));
+        }
+        while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_starts']);
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateExpires = $aMembership['date_expires'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $iDateExpires);
+        } while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_expires']);
+
+        $aMembershipCurrent['date_starts'] = $iDateStarts;
+        $aMembershipCurrent['date_expires'] = $iDateExpires;
+
+        return $aMembershipCurrent;
+    }
+
+    /**
+     * Set a membership for a profile
+     *
+     * @param  int     $iProfileId profile that is going to get the membership
+     * @param  int     $iLevelId   membership that is going to be assigned to the profile
+     *                             if $iLevelId == MEMBERSHIP_ID_STANDARD then $days and $bStartsNow parameters are not used,
+     *                             so Standard membership is always set immediately and `forever`
+     * @param  mixed   $mixedPeriod  number of Days to set membership for or an array with 'period'-'period unit' pair. If number or 'period' in pair equal 0, then the membership is set forever
+     * @param  boolean $bStartsNow if true, the membership will start immediately if false, the membership will start after the current membership expires
+     * @return boolean true in case of success, false in case of failure
+     */
+    function setMembership($iProfileId, $iLevelId, $mixedPeriod = 0, $bStartsNow = false, $sTransactionId = '')
+    {
+        $iProfileId = (int)$iProfileId;
+        $iLevelId = (int)$iLevelId;
+        $bStartsNow = $bStartsNow ? true : false;
+
+        if (!$iProfileId)
+            $iProfileId = -1;
+
+        if (empty($sTransactionId))
+            $sTransactionId = 'NULL';
+
+        // check if profile exists
+        if(($sProfileEmail = BxDolProfileQuery::getInstance()->getEmailById($iProfileId)) === false)
+            return false;
+
+        // check if membership exists
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        if(empty($aLevel) || !is_array($aLevel))
+            return false;
+
+        if($iLevelId == MEMBERSHIP_ID_NON_MEMBER)
+            return false;
+
+        $aMembershipCurrent = $this->getMemberMembershipInfo($iProfileId);
+        $aMembershipLatest = $this->getMemberMembershipInfoLatest($iProfileId);
+
+        // setting Standard membership level
+        if ($iLevelId == MEMBERSHIP_ID_STANDARD) {
+            if ($aMembershipCurrent['id'] == MEMBERSHIP_ID_STANDARD)
+                return true;
+
+            // delete present and future memberships
+            $bResult = $this->oDb->deleteLevelByProfileId($iProfileId);
+            if($bResult)
+                $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
+
+            return $bResult;
+        }
+
+        if ((is_numeric($mixedPeriod) && (int)$mixedPeriod < 0) || (is_array($mixedPeriod) && (!isset($mixedPeriod['period']) || $mixedPeriod['period'] < 0)))
+            return false;
+
+        /*
+         * Make the membership starts after the latest membership expires or starts immediately 
+         * if latest membership is lifetime membership or immediate start was requested.
+         */
+        $iDateStarts = time();
+        if ($bStartsNow || empty($aMembershipLatest['date_expires'])) {
+            // Delete any profile's membership level and actions traces
+            $this->oDb->deleteLevelByProfileId($iProfileId, true); 
+            $this->oDb->clearActionsTracksForMember($iProfileId);
+            $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
+        }
+        else
+            $iDateStarts = $aMembershipLatest['date_expires'];
+
+        // set lifetime membership if 0 days is used.
+        if(is_numeric($mixedPeriod))
+            $mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => MEMBERSHIP_PERIOD_UNIT_DAY);
+
+        if(!$this->oDb->insertLevelByProfileId($iProfileId, $iLevelId, $iDateStarts, $mixedPeriod, $sTransactionId))
+           return false;
+
+        // raise membership alert
+        bx_alert('profile', 'set_membership', '', $iProfileId, array(
+            'mlevel'=> $iLevelId, 
+            'period' => $mixedPeriod['period'], 
+            'period_unit' => $mixedPeriod['period_unit'], 
+            'starts_now' => $bStartsNow, 
+            'txn_id' => $sTransactionId
+        ));
+
+        // audit
+        $aDataForAudit = array();
+        if (!empty($aMembershipCurrent))
+            $aDataForAudit = array('new_membership_level' => _t($aLevel['name']), 'old_membership_level' => _t($aMembershipCurrent['name']));
+        BxDolProfile::getInstance($iProfileId)->doAudit('_sys_audit_action_set_membership', $aDataForAudit);
+        
+        // Send notification
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_MemChanged', array('membership_level' => _t($aLevel['name'])), 0, $iProfileId);
+        if ($aTemplate)
+            sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body']);
+
+        return true;
+    }
+
+    function unsetMembership($iProfileId, $iLevelId, $sTransactionId)
+    {
+    	return $this->oDb->deleteLevelBy(array(
+    		'IDMember' => $iProfileId,
+    		'IDLevel' => $iLevelId,
+    		'TransactionID' => $sTransactionId
+    	));
+    }
+
+    /**
+     * get action id by module and name
+     * @param $sAction action name
+     * @param $sModule module name
+     * @param $aActions array of actions from sys_acl_actions table, with default array keys (starting from 0) and text values
+     */
+    function getMembershipActionId($sAction, $sModule)
+    {
+        $this->oDb->getActions(array('type' => 'by_names_and_module', 'value' => $sAction, 'module' => $sModule), $aActions, false);
+        if (count($aActions) > 1)
+            trigger_error('Duplicate action - name:' . $sAction . ', module:' . $sModule, E_USER_ERROR);
+        $aAction = array_pop($aActions);
+        return $aAction['id'];
+    }
+
+    function getExpirationLetter($iProfileId, $sLevelName, $iLevelExpireDays )
+    {
+        $iProfileId = (int)$iProfileId;
+
+        if(!$iProfileId)
+            return false;
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $sProfileEmail = $oProfileQuery->getEmailById($iProfileId);
+
+        $aPlus = array(
+            'membership_name' => _t($sLevelName),
+            'expire_days' => $iLevelExpireDays,
+            'page_url' => BxDolRequest::serviceExists('bx_acl', 'get_view_url') ? BxDolService::call('bx_acl', 'get_view_url') : '#'
+        );
+
+        $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()
+    {
+        return $this->oDb->maintenance();
+    }
+
+    protected function getMemberMembershipInfoCurrent($iProfileId, $iTime = 0, $bClearCache = 0)
+    {
+        $sKey = $iProfileId . '_' . $iTime;
+        if ($bClearCache && isset(self::$_aCacheData[$sName]))
+            unset(self::$_aCacheData[$sKey]);
+        elseif (array_key_exists($sKey, self::$_aCacheData) && !defined('BX_DOL_INSTALL') && !defined('BX_DOL_CRON_EXECUTE'))
+			return self::$_aCacheData[$sKey];
+
+        $aMemLevel = false;
+
+        do {
+            // 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);
+                break;
+            }
+
+            // check if account is unconfirmed, every account's profile is unconfirmed if account is unconfirmed
+            $oAccount = $aProfileInfo ? BxDolAccount::getInstance($aProfileInfo['account_id']) : false;
+            if ($oAccount && !$oAccount->isConfirmed()) {
+                $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_UNCONFIRMED);
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: MEMBERSHIP_ID_UNCONFIRMED', E_USER_ERROR);
+                break;
+            }
+            
+            // profile is not active, so return standard memberships according to profile status        
+            if (false === $sProfileStatus || BX_PROFILE_STATUS_ACTIVE != $sProfileStatus) {                
+                if (!isset($this->_aProfileStatus2LevelMap[$sProfileStatus]))
+                    $iLevelId = MEMBERSHIP_ID_NON_MEMBER; // if there is no profile status - then it isn't member
+                else
+                    $iLevelId = $this->_aProfileStatus2LevelMap[$sProfileStatus]; // get member level id which associated with every non-active status
+
+                $aMemLevel = $this->oDb->getLevelByIdCached($iLevelId);
+
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: ' . $iLevelId, E_USER_ERROR);
+                break;
+            }
+
+            // 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);
+                break;
+            }
+        } 
+        while (0);
+
+        return (self::$_aCacheData[$sKey] = $aMemLevel);
+    }
+
+    protected function getMemberMembershipInfoLatest($iProfileId, $iTime = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        while ($aMembership['id'] != MEMBERSHIP_ID_STANDARD) {
+            $aMembershipLast = $aMembership;
+            if(!isset($aMembership['date_expires']) || (int)$aMembership['date_expires'] == 0)
+                break;
+
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $aMembership['date_expires']);
+        }
+
+        return $aMembershipLast;
+    }
+
+    public function onProfileDelete ($iProfileId)
+    {
+        return $this->oDb->deleteLevelByProfileId($iProfileId, true);
+    }
+}
+
+function checkAction($iProfileId, $iActionId, $bPerformAction = false)
+{
+    return BxDolAcl::getInstance()->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+function checkActionModule($iProfileId, $sActionName, $sModuleName, $bPerformAction = false)
+{
+    $oACL = BxDolAcl::getInstance();
+
+    $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
+    if (!$iActionId)
+        bx_trigger_error("Unknown action: '$sActionName' in module '$sModuleName'", 1);
+
+    return $oACL->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+/** @} */

+ 455 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolAclQuery.php

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

+ 177 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolBadgesQuery.php

@@ -0,0 +1,177 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolCategories
+ */
+class BxDolBadgesQuery extends BxDolDb
+{
+    protected $_sTableCategories;
+
+    public function __construct()
+    {
+    	parent::__construct();
+
+        $this->_sTableBadges = 'sys_badges';
+    }
+
+    public function getData($aParams = array(), &$aItems = false)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`sc`.*";
+        $sJoinClause = $sWhereClause = $sGroupClause = $sLimitClause = "";
+        $sOrderClause = "`sc`.`added` ASC";
+
+        if(isset($aParams['count_only']) && $aParams['count_only'] === true) {
+            $aMethod['name'] = 'getOne';
+            $sSelectClause = "COUNT(`sc`.`id`)";
+        }
+
+        switch($aParams['type']) {
+            case 'counter_by_modules':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'module';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = "`sc`.`module` AS `module`, COUNT(`sc`.`id`) AS `counter`";
+                $sJoinClause = "LEFT JOIN `sys_badges2objects` `soc` ON `sc`.`id` =  `soc`.`badge_id`";
+                $sGroupClause = "`sc`.`module`";
+                break;
+              
+            case 'by_module&object':
+                $aMethod['name'] = 'getAll';
+                $sSelectClause = "`sc`.`id`, `sc`.`id`, `sc`.`text`, `sc`.`color`, `sc`.`fontcolor`, `sc`.`is_icon_only`, `sc`.`icon`, `soc`.`id` AS `badge_id`";
+                $sJoinClause = "LEFT JOIN `sys_badges2objects` `soc` ON `sc`.`id` =  `soc`.`badge_id` AND `soc`.`object_id` = :object_id";
+                $sWhereClause = " AND `sc`.`module` = :module";
+                $aMethod['params'][1] = array(
+                    'object_id' => $aParams['object_id'],
+                    'module' => $aParams['module']
+                );
+                break;
+                
+            case 'by_module&object2':
+                $aMethod['name'] = 'getAll';
+                $sSelectClause = "`sc`.`id`, `sc`.`id`, `sc`.`text`, `sc`.`color`, `sc`.`fontcolor`, `sc`.`is_icon_only`, `sc`.`icon`, `soc`.`id` AS `badge_id`";
+                $sJoinClause = "INNER JOIN `sys_badges2objects` `soc` ON `sc`.`id` =  `soc`.`badge_id` AND `soc`.`object_id` = :object_id";
+                $sWhereClause = " AND `sc`.`module` = :module";
+                $aMethod['params'][1] = array(
+                    'object_id' => $aParams['object_id'],
+                    'module' => $aParams['module']
+                );
+                break;
+                
+            case 'by_module&object2_single':
+                $aMethod['name'] = 'getAll';
+                $sSelectClause = "`sc`.`id`, `sc`.`id`, `sc`.`text`, `sc`.`color`, `sc`.`fontcolor`, `sc`.`is_icon_only`, `sc`.`icon`, `soc`.`id` AS `badge_id`";
+                $sJoinClause = "INNER JOIN `sys_badges2objects` `soc` ON `sc`.`id` =  `soc`.`badge_id` AND `soc`.`object_id` = :object_id";
+                $sWhereClause = " AND `sc`.`module` = :module";
+                $sLimitClause = "LIMIT 0, 1";
+                $aMethod['params'][1] = array(
+                    'object_id' => $aParams['object_id'],
+                    'module' => $aParams['module']
+                );
+                break;
+                
+            case 'by_module&object&badge':
+                $aMethod['name'] = 'getAll';
+                $sSelectClause = "`sc`.`id`, `sc`.`id`, `sc`.`text`, `sc`.`icon`, `soc`.`id` AS `badge_id`";
+                $sJoinClause = "INNER JOIN `sys_badges2objects` `soc` ON `sc`.`id` =  `soc`.`badge_id` AND `soc`.`object_id` = :object_id";
+                $sWhereClause = " AND `sc`.`module` = :module  AND `sc`.`id` = :badge_id";
+                $aMethod['params'][1] = array(
+                    'object_id' => $aParams['object_id'],
+                    'badge_id' => $aParams['badge_id'],
+                    'module' => $aParams['module']
+                );
+                break;
+                
+            case 'id':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause = " AND `sc`.`id` = :id";
+                break;
+            
+            case 'by_module':
+                $aMethod['name'] = 'getAll';
+                $sWhereClause = " AND `sc`.`module` = :module";
+                $aMethod['params'][1] = array(
+                    'module' => $aParams['module']
+                );
+                break;
+        }
+
+        if(!empty($sGroupClause))
+            $sGroupClause = "GROUP BY " . $sGroupClause;
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " 
+            FROM `" . $this->_sTableBadges . "` AS `sc`" . $sJoinClause . " 
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause. " " . $sLimitClause;
+        $oRv = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+        if ($aItems === false)
+            return $oRv;
+        $aItems = $oRv;   
+    }
+    
+    function update($iId, $aFields)
+    {
+        $sSql = "UPDATE `sys_badges` SET `" . implode("`=?, `", array_keys($aFields)) . "`=?  WHERE `id`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aFields), array($iId)));
+        return $this->query($sSql);
+    }
+    
+    function add($iBadgeId, $iObjectId, $sModule)
+    {
+        $sSql = "INSERT `sys_badges2objects` (`badge_id`, `object_id`, `added`, `module`) VALUES (:badge_id, :object_id, :added, :module)";
+        $aBindings = array(
+            'badge_id' => $iBadgeId,
+            'object_id' => $iObjectId,
+            'added' => time(),
+            'module' => $sModule,
+        );
+        return $this->query($sSql, $aBindings);
+    }
+    
+    public function delete($aParams = array())
+    {
+        switch($aParams['type']) {
+            case 'id':
+                $sQuery = " WHERE `badge_id` = :badge_id";
+                $aBindings = array(
+                    'badge_id' => $aParams['id'],
+                );
+                break;
+                
+            case 'by_module&object':
+                $sQuery = " WHERE `module` = :module AND object_id = :object_id";
+                $aBindings = array(
+                    'module' => $aParams['module'],
+                    'object_id' => $aParams['object_id'],
+                );
+                break;
+            case 'by_module&object&badge':   
+                $sQuery = " WHERE `module` = :module AND object_id = :object_id AND badge_id = :badge_id";
+                $aBindings = array(
+                    'module' => $aParams['module'],
+                    'object_id' => $aParams['object_id'],
+                    'badge_id' => $aParams['badge_id'],
+                );
+                break;
+        }
+        
+        $sQuery = "DELETE FROM `sys_badges2objects`" . $sQuery;
+        $this->query($sQuery, $aBindings);
+    }
+}
+
+/** @} */

+ 95 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCacheMemcache.php

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

+ 168 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCategoriesQuery.php

@@ -0,0 +1,168 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolCategories
+ */
+class BxDolCategoriesQuery extends BxDolDb
+{
+    protected $_sTableCategories;
+
+    public function __construct()
+    {
+    	parent::__construct();
+
+        $this->_sTableCategories = 'sys_categories';
+    }
+
+    public function getData($aParams = array())
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`sc`.*";
+        $sJoinClause = $sWhereClause = $sGroupClause = "";
+        $sOrderClause = "`sc`.`added` ASC";
+
+        if(isset($aParams['count_only']) && $aParams['count_only'] === true) {
+            $aMethod['name'] = 'getOne';
+            $sSelectClause = "COUNT(`sc`.`id`)";
+        }
+
+        switch($aParams['type']) {
+            case 'by_module':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'value';
+                $aMethod['params'][2] = array(
+                    'module' => $aParams['module']
+                );
+
+                $sSelectClause = "`sc`.*, `sc`.`value` as `key`, `sc`.`Value` as `value`";
+                $sWhereClause = " AND `sc`.`module` = :module AND (`sc`.`status` = 'active')";
+                $sOrderClause = "`sc`.`Value` ASC";
+                break;
+                
+            case 'by_module_and_author':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'value';
+                $aMethod['params'][2] = array(
+                    'module' => $aParams['module'],
+                    'author' => $aParams['author'],
+                );
+                
+                $sSelectClause = "`sc`.*, `sc`.`value` as `key`, `sc`.`Value` as `value`";
+                $sWhereClause = " AND `sc`.`module` = :module AND (`sc`.`author` = 0 OR `sc`.`author` = :author) AND (`sc`.`status` = 'active')";
+                $sOrderClause = "`sc`.`Value` ASC";
+                break;    
+                
+            case 'by_module_with_num':
+                $aMethod['name'] = 'getAll';
+                $aMethod['params'][1] = array(
+                    'module' => $aParams['module'],
+
+                );
+                
+                $oModule = BxDolModule::getInstance($aParams['module']);
+                $CNF = $oModule->_oConfig->CNF;
+                
+                $sSelectClause = "`sc`.`value`, COUNT(`sc`.`id`) as `num`";
+                $sJoinClause = "INNER JOIN `sys_categories2objects` `soc` ON `sc`.`id` =  `soc`.`category_id`";
+                if (isset($CNF['FIELD_STATUS']))
+                    $sJoinClause .= "INNER JOIN `" . $CNF['TABLE_ENTRIES'] . "` `data` ON `soc`.`object_id` =  `data`.`id` AND `data`.`" . $CNF['FIELD_STATUS'] . "` = 'active'";
+                $sWhereClause = " AND `sc`.`status` = 'active' AND `soc`.`module` = :module";
+                $sGroupClause = "`sc`.`id`";
+                $sOrderClause = "`num` DESC";
+                break;
+                
+            case 'by_module_and_object':
+                $aMethod['name'] = 'getColumn';
+                $aMethod['params'][1] = array(
+                    'module' => $aParams['module'],
+                    'object_id' => $aParams['object_id']
+                );
+                
+                $sSelectClause = "`sc`.`value`";
+                $sJoinClause = "INNER JOIN `sys_categories2objects` `soc` ON `sc`.`id` =  `soc`.`category_id`";
+                $sWhereClause = " AND `sc`.`status` = 'active' AND `soc`.`module` = :module AND `soc`.`object_id` = :object_id";
+                $sOrderClause = "`added` DESC";
+                break;
+                
+            case 'value_and_module':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'value' => $aParams['value'],
+                    'module' => $aParams['module'],
+                );
+
+                $sWhereClause = " AND `sc`.`value` = :value AND `sc`.`module` = :module";
+                break;  
+                
+            case 'id':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause = " AND `sc`.`id` = :id";
+                break;
+        }
+
+        if(!empty($sGroupClause))
+            $sGroupClause = "GROUP BY " . $sGroupClause;
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " 
+            FROM `" . $this->_sTableCategories . "` AS `sc`" . $sJoinClause . " 
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause;
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+    
+    public function delete($sModule, $iObject)
+    {
+        $sQuery = "DELETE FROM `sys_categories2objects` WHERE `module` = :module AND `object_id` = :object_id";
+        $aBindings = array(
+           'module' => $sModule,
+           'object_id' => $iObject,
+        );
+        $this->query($sQuery, $aBindings);
+    }
+    
+    public function add($sModule, $iProfileId, $sValue, $iObject, $bAutoActivation)
+    {
+        $sStatus = $bAutoActivation ? 'active' : 'hidden';
+        $aBindings = array(
+            'value' => $sValue,
+            'module' => $sModule,
+            'author' => $iProfileId 
+        );
+        $sQuery = "SELECT id FROM `sys_categories` WHERE `value` = :value AND `module` = :module AND (`author` = 0 OR `author` = :author)";
+        $iCategoryId = (int)$this->getOne($sQuery, $aBindings);
+        
+        if($iCategoryId == 0){   
+            $aBindings['status'] = $sStatus;
+            $aBindings['added'] = time();
+            $sQuery = "INSERT INTO `sys_categories` (`value`, `module`, `status`, `added`, `author`) VALUES(:value, :module, :status, :added, :author)";
+            $this->query($sQuery, $aBindings);
+            $iCategoryId = $this->lastId();
+        }
+
+        $sQuery = "INSERT INTO `sys_categories2objects` (`module`, `object_id`, `category_id`) VALUES(:module, :object_id, :category_id)";
+        $aBindings = array(
+           'module' => $sModule,
+           'object_id' => $iObject,
+           'category_id' => $iCategoryId,
+        );
+        $this->query($sQuery, $aBindings);
+        
+        return $iCategoryId;
+    }
+}
+
+/** @} */

+ 1923 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCmts.php

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

+ 627 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolCmtsQuery.php

@@ -0,0 +1,627 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolCmts
+ */
+class BxDolCmtsQuery extends BxDolDb
+{
+    protected $_oMain;
+
+    protected $_sTable;
+    protected $_sTriggerTable;
+    protected $_sTriggerFieldId;
+    protected $_sTriggerFieldAuthor;
+    protected $_sTriggerFieldTitle;
+    protected $_sTriggerFieldComments;
+
+    protected $_sTableFiles;
+    protected $_sTableFiles2Entries;
+
+    protected $_sTableIds;
+
+    public function __construct(&$oMain)
+    {
+        $this->_sTableIds = BxDolCmts::$sTableIds;
+
+        $this->_oMain = $oMain;
+
+        $this->_sTableFiles = $this->_oMain->getTableNameImages();
+        $this->_sTableFiles2Entries = $this->_oMain->getTableNameImages2Entries();
+
+        $aSystem = $this->_oMain->getSystemInfo();
+        $this->_sTable = $aSystem['table'];
+        $this->_sTriggerTable = $aSystem['trigger_table'];
+        $this->_sTriggerFieldId = $aSystem['trigger_field_id'];
+        $this->_sTriggerFieldAuthor = $aSystem['trigger_field_author'];
+        $this->_sTriggerFieldTitle = $aSystem['trigger_field_title'];
+        $this->_sTriggerFieldComments = $aSystem['trigger_field_comments'];
+
+        parent::__construct();
+    }
+
+    function getTableName ()
+    {
+        return $this->_sTable;
+    }
+
+    /**
+     * @deprecated since version 10.0.0-B3 and can be removed in later versions.
+     */
+    function setTableNameFiles($sTable)
+    {
+    	$this->_sTableFiles = $sTable;
+    }
+
+    /**
+     * @deprecated since version 10.0.0-B3 and can be removed in later versions.
+     */
+    function setTableNameFiles2Entries($sTable)
+    {
+    	$this->_sTableFiles2Entries = $sTable;
+    }
+
+    function getCommentsCountAll ($iId)
+    {
+        if ($this->_sTriggerFieldComments) {
+            $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldComments}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ?", $iId);
+            return (int)$this->getOne($sQuery);
+        }
+        else {
+            return $this->getCommentsCount($iId);
+        }
+    }
+
+    function getCommentsCount ($iId, $iCmtVParentId = -1, $iAuthorId = 0, $sFilter = '')
+    {
+    	$aBindings = array(
+    		'cmt_object_id' => $iId
+    	);
+
+        $sWhereClause = '';
+        if((int)$iCmtVParentId >= 0) {
+        	$aBindings['cmt_vparent_id'] = $iCmtVParentId;
+
+            $sWhereClause .= " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id";
+        }
+
+        $sJoinClause = '';
+        switch($sFilter) {
+        	case BX_CMT_FILTER_FRIENDS:
+        	case BX_CMT_FILTER_SUBSCRIPTIONS:
+	            $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter));
+	
+	            $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId);
+	            $sJoinClause .= ' ' . $aQueryParts['join'];
+	            break;
+
+        	case BX_CMT_FILTER_OTHERS:
+        		$aBindings['cmt_author_id'] = $iAuthorId;
+
+        		$sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` <> :cmt_author_id";
+        		break;
+        }
+
+        $sQuery = "SELECT COUNT(*) FROM `{$this->_sTable}` $sJoinClause WHERE `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id" . $sWhereClause;
+        return (int)$this->getOne($sQuery, $aBindings);
+    }
+
+    function getStructure($iObjectId, $iAuthorId = 0, $iParentId = 0, $sFilter = '', $aOrder = array())
+    {
+        $sSelectClause = $sJoinClause = $sWhereClause = "";
+
+    	$aBindings = array(
+            'cmt_object_id' => $iObjectId,
+            'system_id' => $this->_oMain->getSystemId()
+    	);
+
+        if((int)$iParentId >= 0) {
+            $aBindings['cmt_parent_id'] = $iParentId;
+
+            $sWhereClause .= " AND `tc`.`cmt_parent_id` = :cmt_parent_id";
+        }
+
+        if(in_array($sFilter, array(BX_CMT_FILTER_FRIENDS, BX_CMT_FILTER_SUBSCRIPTIONS))) {
+            $sConnection = $this->_oMain->getConnectionObject($sFilter);
+            $aQueryParts = BxDolConnection::getObjectInstance($sConnection)->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId);
+            $sJoinClause .= ' ' . $aQueryParts['join'];
+        }
+
+        if(isset($aOrder['by']) && isset($aOrder['way']))
+            switch($aOrder['by']) {
+                case BX_CMT_ORDER_BY_DATE:
+                    $sSelectClause .= ", `tc`.`cmt_time` AS `cmt_order`";
+                    break;
+
+                case BX_CMT_ORDER_BY_POPULAR:
+                    $aOrderFields = array();
+                    if($this->_oMain->getVoteObject(0) !== false)
+                        $aOrderFields[] = "`ti`.`votes`";
+                    if($this->_oMain->getScoreObject(0) !== false)
+                        $aOrderFields[] = "`ti`.`score`";
+                    
+                    if(!empty($aOrderFields))
+                        $sSelectClause .= ", (" . implode(' + ', $aOrderFields) . ") AS `cmt_order`";
+                    else
+                        $sSelectClause .= ", `tc`.`id` AS `cmt_order`";
+                    break;
+            }
+
+        $sQuery = "SELECT
+                `tc`.`cmt_id`, 
+                `tc`.`cmt_replies`, 
+                `tc`.`cmt_time` $sSelectClause
+            FROM `{$this->_sTable}` AS `tc`
+            LEFT JOIN `{$this->_sTableIds}` AS `ti` ON `tc`.`cmt_id` = `ti`.`cmt_id` AND `ti`.`system_id` = :system_id $sJoinClause
+            WHERE `tc`.`cmt_object_id` = :cmt_object_id" . $sWhereClause;
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    function getComments ($iId, $iCmtVParentId = 0, $iAuthorId = 0, $sFilter = '', $aOrder = array(), $iStart = 0, $iCount = -1)
+    {
+    	$aBindings = array(
+    		'cmt_object_id' => $iId,
+            'system_id' => $this->_oMain->getSystemId()
+    	);
+        $sFields = $sJoin = "";
+        
+        $sWhereParent = '';
+        if((int)$iCmtVParentId >= 0) {
+        	$aBindings['cmt_vparent_id'] = $iCmtVParentId;
+
+            $sWhereParent = " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id";
+        }
+
+        if(in_array($sFilter, array(BX_CMT_FILTER_FRIENDS, BX_CMT_FILTER_SUBSCRIPTIONS))) {
+            $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter));
+
+            $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId);
+            $sJoin .= ' ' . $aQueryParts['join'];
+        }
+
+        $sOrder = " ORDER BY `{$this->_sTable}`.`cmt_pinned` DESC, `{$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:
+                    $sOrder = " ORDER BY `{$this->_sTable}`.`cmt_pinned` DESC, `{$this->_sTable}`.`cmt_time` " . $aOrder['way'];
+                    break;
+
+                case BX_CMT_ORDER_BY_POPULAR:
+                    $oVote = $this->_oMain->getVoteObject(0);
+                    $oScore = $this->_oMain->getScoreObject(0);
+                    $aSortFields = array();
+                    if($oVote !== false) {
+                        array_push($aSortFields, '`' . $this->_sTableIds . '`.`votes`');
+                    }
+                    if($oScore !== false) {
+                        array_push($aSortFields, '`' . $this->_sTableIds . '`.`score`');
+                    }
+                    if (count($aSortFields) == 0)
+                        array_push($aSortFields, '`' . $this->_sTable . '`.`id`');
+                    
+                    $sOrder = " ORDER BY `{$this->_sTable}`.`cmt_pinned` DESC, " . implode($aOrder['way'] . ', ', $aSortFields) . " " . $aOrder['way'];
+                    break;
+            }
+        }
+        
+       	$sLimit = $iCount != -1 ? $this->prepareAsString(" LIMIT ?, ?", (int)$iStart, (int)$iCount) : '';
+
+        $sQuery = "SELECT
+                `{$this->_sTable}`.*,
+                `{$this->_sTableIds}`.`id` AS `cmt_unique_id`
+                $sFields
+            FROM `{$this->_sTable}`
+            LEFT JOIN `{$this->_sTableIds}` ON (`{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = :system_id)
+            $sJoin
+            WHERE `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id" . $sWhereParent . $sOrder . $sLimit;
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    function getCommentsBy($aParams = array())
+    {
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array()));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`{$this->_sTable}`.*";
+
+        if(isset($aParams['object_id'])) {
+            $aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id'];
+
+            $sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id";
+        }
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['id'] = (int)$aParams['id'];
+
+                $sWhereClause .= " AND `{$this->_sTable}`.`cmt_id` = :id";
+                break;
+
+            case 'latest':
+            	if(!empty($aParams['author'])) {
+            		$aMethod['params'][1]['cmt_author_id'] = (int)$aParams['author'];
+
+            		$sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` " . (isset($aParams['others']) && (int)$aParams['others'] == 1 ? "<>" : "=") . " :cmt_author_id";
+            	}
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` DESC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                	$sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'parent_id':
+                $aMethod['params'][1]['cmt_parent_id'] = (int)$aParams['parent_id'];
+
+                $sWhereClause .= " AND `{$this->_sTable}`.`cmt_parent_id` = :cmt_parent_id";
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'object_id':
+                $aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id'];
+
+                $sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id";
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` " . (!empty($aParams['order_way']) ? strtoupper($aParams['order_way']) : "ASC");
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'author_id':
+                $aMethod['params'][1]['cmt_author_id'] = (int)$aParams['author_id'];
+
+                $sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` = :cmt_author_id";
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'search_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`{$this->_sTable}`.`cmt_id`";
+
+                if (!empty($aParams['start']) && !empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+                elseif (!empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?", $aParams['per_page']);
+
+                $sWhereConditions = "1";
+                foreach($aParams['search_params'] as $sSearchParam => $aSearchParam) {
+                    $sSearchValue = "";
+                    switch ($aSearchParam['operator']) {
+                        case 'like':
+                            $sSearchValue = " LIKE " . $this->escape("%" . $aSearchParam['value'] . "%");
+                            break;
+
+                        case 'in':
+                            $sSearchValue = " IN (" . $this->implode_escape($aSearchParam['value']) . ")";
+                            break;
+
+                        case 'and':
+                            $iResult = 0;
+                            if (is_array($aSearchParam['value']))
+                                foreach ($aSearchParam['value'] as $iValue)
+                                    $iResult |= pow (2, $iValue - 1);
+                            else 
+                                $iResult = (int)$aSearchParam['value'];
+
+                            $sSearchValue = " & " . $iResult . "";
+                            break;
+
+                        default:
+                             $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam;
+                             $aMethod['params'][1][$sSearchParam] = $aSearchParam['value'];                             
+                    }
+
+                    $sWhereConditions .= " AND `{$this->_sTable}`.`" . $sSearchParam . "`" . $sSearchValue;
+                }
+
+                $sWhereClause .= " AND (" . $sWhereConditions . ")"; 
+
+                $sOrderClause .=  "`{$this->_sTable}`.`cmt_time` ASC";
+                break;
+
+            case 'all_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`{$this->_sTable}`.`cmt_id`";
+                $sOrderClause =  "`{$this->_sTable}`.`cmt_time` ASC";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `{$this->_sTable}` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+		return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    function getComment ($iId, $iCmtId)
+    {
+        $sFields = $sJoin = "";
+
+        $oVote = $this->_oMain->getVoteObject($iCmtId);
+        if($oVote !== false) {
+            $aSql = $oVote->getSqlParts($this->_sTableIds, 'id');
+
+            $sFields .= $aSql['fields'];
+            $sJoin .= $aSql['join'];
+        }
+
+        $sQuery = $this->prepare("SELECT
+                `{$this->_sTable}`.*,
+                `{$this->_sTableIds}`.`id` AS `cmt_unique_id`
+                $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);
+    }
+    
+    static function getCommentByUniq ($iUnicId)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `table_ids`.`cmt_id`, table_obj.`Name` AS `system_name`, table_obj.`Table` AS `table_name` 
+            FROM `" . BxDolCmts::$sTableIds . "` as `table_ids`  
+            INNER JOIN  `" . BxDolCmts::$sTableSystems . "` as `table_obj` ON  `table_ids`.`system_id` = `table_obj`.`ID`
+            WHERE `table_ids`.`id` = ?
+            LIMIT 1", $iUnicId);
+        $aRow = $oDb->getRow($sQuery);
+        
+        if ($aRow){
+            $sQuery = $oDb->prepare("SELECT `cmt_object_id` FROM `" . $aRow['table_name'] . "` WHERE `cmt_id` = ? LIMIT 1", $aRow['cmt_id']);
+            $aRow['cmt_object_id'] = $oDb->getOne($sQuery);
+        }
+        
+        return $aRow;
+    }
+
+    function getCommentSimple ($iId, $iCmtId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM {$this->_sTable} AS `c` WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId);
+        return $this->getRow($sQuery);
+    }
+
+    function removeComment ($iId, $iCmtId, $iCmtParentId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId);
+        if (!$this->query($sQuery))
+            return false;
+
+        if($iCmtParentId)
+            $this->updateRepliesCount($iCmtParentId, -1);
+
+        return true;
+    }
+
+    function saveImages($iSystemId, $iCmtId, $iImageId)
+    {
+        $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableFiles2Entries}` SET `system_id`=?, `cmt_id`=?, `image_id`=?", $iSystemId, $iCmtId, $iImageId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function getFiles($iSystemId, $iCmtId, $iId = false)
+    {
+    	$aBindings = array(
+    		'system_id' => $iSystemId
+    	);
+
+        $sJoin = "";
+        $sWhere = " AND `tf2e`.`system_id` = :system_id ";
+
+        if($iCmtId !== false) {
+        	$aBindings['cmt_id'] = $iCmtId;
+
+            $sWhere .= " AND `tf2e`.`cmt_id` = :cmt_id ";
+        }
+
+        if($iId !== false) {
+        	$aBindings['cmt_object_id'] = $iId;
+
+            $sWhere .= " AND `te`.`cmt_object_id` = :cmt_object_id";
+            $sJoin .= " INNER JOIN `{$this->_sTable}` AS `te` ON (`tf2e`.`cmt_id` = `te`.`cmt_id`) ";
+        }
+
+        $sQuery = "SELECT 
+        		`tf2e`.*,
+        		`tf`.`file_name` AS `file_name`,
+        		`tf`.`mime_type` AS `mime_type`,
+        		`tf`.`size` AS `size` 
+        	FROM `{$this->_sTableFiles2Entries}` AS `tf2e` 
+        	LEFT JOIN `{$this->_sTableFiles}` AS `tf` ON (`tf2e`.`image_id` = `tf`.`id`) " . $sJoin . " 
+        	WHERE 1 " . $sWhere;
+
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    function deleteImages($iSystemId, $iCmtId, $iImageId = false)
+    {
+        $sWhereAddon = "";
+        $aBindings = array();
+
+        if ($iSystemId !== false) {
+            $aBindings['system_id'] = $iSystemId;
+
+            $sWhereAddon .= " AND `system_id` = :system_id ";
+        }
+
+        if ($iCmtId !== false) {
+            $aBindings['cmt_id'] = $iCmtId;
+
+            $sWhereAddon .= " AND `cmt_id` = :cmt_id ";
+        }
+
+        if ($iImageId !== false) {
+            $aBindings['image_id'] = $iImageId;
+
+            $sWhereAddon .= " AND `image_id` = :image_id ";
+        }
+
+        return $this->query("DELETE FROM `{$this->_sTableFiles2Entries}` WHERE 1" . $sWhereAddon, $aBindings);
+    }
+
+    function updateComments($aSetClause, $aWhereClause)
+    {
+        if(empty($aSetClause) || empty($aWhereClause))
+            return;
+
+        return (int)$this->query("UPDATE `{$this->_sTable}` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0;
+    }
+
+    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);
+		foreach ($a as $r) {
+            $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_id` = ?", $r['cmt_id']);
+            $this->query ($sQuery);
+
+            $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_replies` = `cmt_replies` - 1 WHERE `cmt_id` = ?", $r['cmt_parent_id']);
+            $this->query ($sQuery);
+
+            $aFilesMore = $this->convertImagesArray($this->getFiles($aSystem['system_id'], $r['cmt_id']));
+            $this->deleteImages($aSystem['system_id'], $r['cmt_id']);
+            if ($aFilesMore && null !== $aFiles)
+                $aFiles = array_merge($aFiles, $aFilesMore);
+
+            if (null !== $aCmtIds)
+                $aCmtIds[] = $r['cmt_id'];
+
+            $isDelOccured = 1;
+        }
+        $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_author_id` = 0 WHERE `cmt_author_id` = ? AND `cmt_replies` != 0", $iAuthorId);
+        $this->query ($sQuery);
+        if ($isDelOccured)
+            $this->query ("OPTIMIZE TABLE {$this->_sTable}");
+    }
+
+    function deleteObjectComments ($iObjectId, &$aFilesReturn = null, &$aCmtIds = null)
+    {
+        $aSystem = $this->_oMain->getSystemInfo();
+        $aFiles = $this->convertImagesArray($this->getFiles($aSystem['system_id'], false, $iObjectId));
+
+        if ($aFiles) {
+            $sQuery = $this->prepare("DELETE FROM {$this->_sTableFiles2Entries} WHERE `system_id` = ? AND `image_id` IN(" . $this->implode_escape($aFiles) . ")", $aSystem['system_id']);
+            $this->query($sQuery);
+        }
+
+        if (null !== $aCmtIds) {
+            $sQuery = $this->prepare("SELECT `cmt_id` FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId);
+            $aCmtIds = $this->getColumn ($sQuery);
+        }
+
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId);
+        $this->query ($sQuery);
+        $this->query ("OPTIMIZE TABLE {$this->_sTable}");
+
+        if (null !== $aFilesReturn)
+            $aFilesReturn = $aFiles;
+    }
+
+    function deleteAll ($iSystemId, &$aFiles = null, &$aCmtIds = null)
+    {
+        // get files
+        if (null !== $aFiles)
+            $aFiles = $this->convertImagesArray($this->getFiles($iSystemId, false));
+
+        // delete files
+        $this->deleteImages($iSystemId, false);
+
+        if (null !== $aCmtIds)
+            $aCmtIds = $this->getColumn ("SELECT `cmt_id` FROM {$this->_sTable}");
+
+        // delete comments
+        $sQuery = $this->prepare("TRUNCATE TABLE {$this->_sTable}");
+        $this->query ($sQuery);
+    }
+
+    function deleteCmtIds ($iSystemId, $iCmtId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTableIds} WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId);
+        return $this->query ($sQuery);
+    }
+
+	function getObjectAuthorId($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldAuthor}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return $this->getOne($sQuery);
+    }
+
+    function getObjectTitle($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldTitle}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return $this->getOne($sQuery);
+    }
+
+    function updateTriggerTable($iId, $iCount)
+    {
+        if (!$this->_sTriggerFieldComments)
+            return true;
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldComments}` = ? WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iCount, $iId);
+        return $this->query($sQuery);
+    }
+
+    function getUniqId($iSystemId, $iCmtId, $iAuthorId = 0)
+    {
+        $sQuery = $this->prepare("SELECT `id` FROM `{$this->_sTableIds}` WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId);
+        if(($iUniqId = (int)$this->getOne($sQuery)) != 0)
+            return $iUniqId;
+
+        if(empty($iAuthorId))
+            $iAuthorId = bx_get_logged_profile_id();
+
+        $sQuery = $this->prepare("INSERT INTO `{$this->_sTableIds}` SET `system_id` = ?, `cmt_id` = ?, `author_id` = ?", $iSystemId, $iCmtId, $iAuthorId);
+        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;
+    }
+}
+
+/** @} */

+ 328 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolConnectionQuery.php

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

+ 1303 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolDb.php

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

+ 97 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolDesigns.php

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

+ 76 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolEmbed.php

@@ -0,0 +1,76 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Embed provider.
+ *
+ * Create an embed from a link.
+ *
+ * @section editor_create Creating the Editor object:
+ *
+ *
+ * Add record to 'sys_objects_embeds' table:
+ *
+ * - object: name of the embed object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing
+ * - title: title of the embed provider, dmay be isplayed in the Studio.
+ * - 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
+ *
+ * Generate HTML for a link which will be converted to an embed later:
+ *
+ * @code
+ *  $oEmbed = BxDolEmbed::getObjectInstance(); // get default embed object instance
+ *  if ($oEmbed) // check if editor is available for using
+ *      echo $oEmbed->getLinkHTML ('https://una.io', 'UNA.IO'); // output HTML which will be automatically converted into embed upon page load
+ *  else
+ *      echo '<a href="https://una.io">UNA.IO</a>';
+ * @endcode
+ *
+ */
+class BxDolEmbed extends BxDolFactoryObject
+{
+    protected $_bCssJsAdded = false;
+
+    /**
+     * Get editor object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject = false, $oTemplate = false)
+    {
+        if (!$sObject)
+            $sObject = getParam('sys_embed_default');
+
+        return parent::getObjectInstanceByClassNames($sObject, $oTemplate, 'BxDolEmbed', 'BxDolEmbedQuery');
+    }
+
+    /**
+     * Print HTML which will be automatically converted into embed upon page load
+     * @param $sLink - link
+     * @param $sTitle - title or use link for the title if omitted
+     * @param $sMaxWidth - try to restrict max width of embed (works in supported embed providers only)
+     */
+    public function getLinkHTML ($sLink, $sTitle = '', $sMaxWidth = '')
+    {
+        // override this function in particular editor class
+    }
+
+    /**
+     * Add css/js files which are needed for embed display and functionality.
+     */
+    public function addJsCss ()
+    {
+        // override this function in particular editor class
+    }
+}
+
+/** @} */

+ 27 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolEmbedQuery.php

@@ -0,0 +1,27 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for embed objects.
+ * @see BxDolEmbed
+ */
+class BxDolEmbedQuery extends BxDolFactoryObjectQuery
+{
+    static public function getObject($sObject)
+    {
+        return parent::getObjectFromTable($sObject, 'sys_objects_embeds');
+    }
+
+    static public function getObjects ()
+    {
+        return parent::getObjectsFromTable('sys_objects_embeds');
+    }
+}
+
+/** @} */

+ 65 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFactoryObject.php

@@ -0,0 +1,65 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Base class for classes with implement object instance 
+ */
+abstract class BxDolFactoryObject extends BxDolFactory implements iBxDolFactoryObject
+{
+	protected $_oDb;
+    protected $_oTemplate;
+	protected $_sObject;
+    protected $_aObject;
+
+    protected function __construct($aObject, $oTemplate = null, $sDbClassName = '')
+    {
+        parent::__construct();
+
+        $this->_aObject = $aObject;
+        $this->_sObject = $aObject['object'];
+        $this->_oTemplate = $oTemplate ? $oTemplate : BxDolTemplate::getInstance();
+        if ($sDbClassName)
+            $this->_oDb = new $sDbClassName($this->_aObject);
+    }
+
+   /**
+     * Get object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstanceByClassNames($sObject, $oTemplate, $sClassName, $sDbClassName)
+    {
+        if(isset($GLOBALS['bxDolClasses'][$sClassName . '!' . $sObject]))
+            return $GLOBALS['bxDolClasses'][$sClassName . '!' . $sObject];
+
+        $aObject = $sDbClassName::getObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = $sClassName;
+        if(!empty($aObject['class_name']) || !empty($aObject['override_class_name'])) {
+            $sClass = !empty($aObject['class_name']) ? $aObject['class_name'] : $aObject['override_class_name'];
+            if(!empty($aObject['class_file']) || !empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . (!empty($aObject['class_file']) ? $aObject['class_file'] : $aObject['override_class_file']));
+        }        
+
+        $o = new $sClass($aObject, $oTemplate);
+        return ($GLOBALS['bxDolClasses'][$sClassName . '!' . $sObject] = $o);
+    }
+
+    /**
+     * Get current object name
+     */
+    public function getObjectName()
+    {
+        return $this->_aObject['object'];
+    }
+}
+
+/** @} */

+ 46 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFactoryObjectQuery.php

@@ -0,0 +1,46 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Base class for DB classes associated with object instance 
+ */
+class BxDolFactoryObjectQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject = array())
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getObjectFromTable($sObject, $sTable)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `$sTable` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getObjectsFromTable ($sTable)
+    {
+        $aObjects = BxDolDb::getInstance()->getAll("SELECT * FROM `$sTable`");
+        if(empty($aObjects) || !is_array($aObjects))
+            return array();
+
+        return $aObjects;
+    }
+}
+
+/** @} */

+ 279 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFavorite.php

@@ -0,0 +1,279 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_FAVORITE_USAGE_BLOCK', 'block');
+define('BX_DOL_FAVORITE_USAGE_INLINE', 'inline');
+define('BX_DOL_FAVORITE_USAGE_DEFAULT', BX_DOL_FAVORITE_USAGE_BLOCK);
+
+/**
+ * Track any object favorites automatically
+ *
+ * Add record to sys_object_favorite table to track object favorites,
+ * to record favorites just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolFavorite::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_favorite table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track favorites
+ *  `table_lists` - table with lists 
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update favorites field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where total favorites number is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ * Structure of the track table is the following:
+ * @code
+ *  CREATE TABLE `my_favorites_track` (
+ *      `object_id` int(11) NOT NULL default '0', -- this field type must be exact as your object id type
+ *      `author_id` int(11) NOT NULL default '0', -- favoring user profile id
+ *      `date` int(11) NOT NULL default '0', -- timestamp of last recorded view
+ *      KEY `id` (`object_id`,`author_id`)
+ *  );
+ * @endcode
+ *
+ */
+class BxDolFavorite extends BxDolObject
+{
+    protected $_sBaseUrl;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolFavoriteQuery($this);
+
+        $this->_sBaseUrl = BxDolPermalinks::getInstance()->permalink($this->_aSystem['base_url']);
+        if(get_mb_substr($this->_sBaseUrl, 0, 4) != 'http')
+            $this->_sBaseUrl = BX_DOL_URL_ROOT . $this->_sBaseUrl;
+        
+        $this->_sFormObject = 'sys_favorite';
+        $this->_sFormDisplayAdd = 'sys_favorite_add';
+        $this->_sFormDisplayListEdit = 'sys_favorite_list_edit';
+    }
+
+   /**
+     * get favorites object instanse
+     * @param $sSys favorite object name
+     * @param $iId associated content id, where favorite is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        $sKey = 'BxDolFavorite!' . $sSys . $iId;
+        if(isset($GLOBALS['bxDolClasses'][$sKey]))
+            return $GLOBALS['bxDolClasses'][$sKey];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplFavorite';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit);
+        return ($GLOBALS['bxDolClasses'][$sKey] = $o);
+    }
+
+    public function getConditionsTrack($sMainTable, $sMainField, $iAuthorId = 0, $iListId = 0)
+    {
+        $aConditions = parent::getConditionsTrack($sMainTable, $sMainField, $iAuthorId);
+        if (empty($aConditions) || !isset($this->_aSystem['table_lists']) || $this->_aSystem['table_lists'] == '')
+            return $aConditions;
+        
+        $sTableTrack = isset($this->_aSystem['table_track']) ? $this->_aSystem['table_track'] : '';
+        $aConditions['restriction']['objects_' . $this->_sSystem . '_list'] = array(
+            'value' => $iListId,
+            'field' => 'list_id',
+            'operator' => '=',
+            'table' => $sTableTrack,
+        );
+        return $aConditions;
+    }
+    
+    public static function &getSystems()
+    {
+        $sKey = 'bx_dol_cache_memory_favorite_systems';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_favorite', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `table_track` AS `table_track`,
+                    `table_lists` AS `table_lists`,
+                    `is_on` AS `is_on`,
+                    `is_undo` AS `is_undo`,
+                    `is_public` AS `is_public`,
+                    `base_url` AS `base_url`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_favorite`', 'name');
+
+        return $GLOBALS[$sKey];
+    }
+
+    public static function onAuthorDelete ($iAuthorId)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem)
+            self::getObjectInstance($sSystem, 0)->getQueryObject()->deleteAuthorEntries($iAuthorId);
+
+        return true;
+    }
+
+	/**
+     * Actions functions
+     */
+    public function actionFavorite()
+    {
+        return echoJson($this->_getFavorite());
+    }
+    
+    public function actionEditList()
+    {
+        $iListId = null;
+        if(!bx_get('list_id'))
+            return false;
+        
+        $iListId = (int) bx_get('list_id');
+        $aList = $this->_oQuery->getList(array('type' => 'id', 'list_id' => $iListId));
+         
+        if ($this->isAllowedEditList($aList['author_id'])){
+            return echoJson($this->_getEditList($aList));
+        }
+        
+        return false;
+    }
+    
+    public function actionDeleteList()
+    {
+        $iListId = null;
+        if(!bx_get('list_id'))
+            return false;
+        
+        $iListId = (int) bx_get('list_id');
+        $aList = $this->_oQuery->getList(array('type' => 'id', 'list_id' => $iListId));
+        
+        $oModule = BxDolModule::getInstance($this->_aSystem["name"]);
+        $CNF = $oModule->_oConfig->CNF;    
+        
+        if ($this->isAllowedEditList($aList['author_id'])){
+            $this->_oQuery->deleteList($iListId);
+            return echoJson(array('redirect' => BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $aList['author_id'])));
+        }
+        
+        return false;
+    }
+
+	public function actionGetFavoritedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+	    if(!$this->isAllowedFavoriteView(true))
+            return $this->msgErrAllowedFavoriteView();
+
+        return $this->_getFavoritedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedFavorite($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('favorite', $isPerformAction);
+    }
+    
+    public function isAllowedEditList($iAuthorId, $isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+        
+        if ($iAuthorId == bx_get_logged_profile_id())
+            return true;
+        
+        return false;
+    }
+
+	public function msgErrAllowedFavorite()
+    {
+        return $this->checkActionErrorMsg('favorite');
+    }
+
+    public function isAllowedFavoriteView($isPerformAction = false)
+    {
+        if(!$this->isPublic())
+            return false;
+
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('favorite_view', $isPerformAction);
+    }
+
+    public function msgErrAllowedFavoriteView()
+    {
+        return $this->checkActionErrorMsg('favorite_view');
+    }
+
+    /**
+     * Auxiliary functions
+     */
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+    public function isPublic()
+    {
+        return (int)$this->_aSystem['is_public'] == 1;
+    }
+
+	/**
+     * Internal functions
+     */
+	
+    protected function _getIconDoFavorite($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ?  'far fa-bookmark' : 'fas fa-bookmark';
+    }
+
+    protected function _getTitleDoFavorite($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? '_favorite_do_unfavorite' : '_favorite_do_favorite';
+    }
+    
+    protected function _getFormObject($sDisplayName)
+    {
+        return BxDolForm::getObjectInstance($this->_sFormObject, $sDisplayName);
+    }
+}
+
+/** @} */

+ 150 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFavoriteQuery.php

@@ -0,0 +1,150 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolFavorite
+ */
+class BxDolFavoriteQuery extends BxDolObjectQuery
+{
+    protected $_sTableLists;
+    
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_sTableLists = isset($aSystem['table_lists']) ? $aSystem['table_lists'] : '';
+        
+        $this->_sMethodGetEntry = 'getFavorite';
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->getOne($sQuery) > 0;
+    }
+
+    public function getPerformedBy($iObjectId, $iStart = 0, $iPerPage = 0)
+    {
+        $sLimitClause = "";
+        if(!empty($iPerPage))
+            $sLimitClause = $this->prepareAsString(" LIMIT ?, ?", $iStart, $iPerPage);
+
+        $sQuery = $this->prepare("SELECT `author_id` FROM `{$this->_sTableTrack}` WHERE `object_id`=? ORDER BY `date` DESC" . $sLimitClause, $iObjectId);
+        return $this->getAll($sQuery);
+    }
+
+	public function getFavorite($iObjectId)
+    {
+        return array('count' => $this->getObjectCount($iObjectId));
+    }
+
+    public function clearFavorite($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ?", $iObjectId, $iAuthorId);
+        return (int)$this->query($sQuery) > 0;
+    }
+    
+    public function doFavorite($iObjectId, $iAuthorId, $iListId = false)
+    {
+        $sQuery = '';
+        if ($iListId === false)
+            $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `author_id` = ?, `date` = ?", $iObjectId, $iAuthorId, time());
+        else
+            $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `author_id` = ?, `date` = ?, `list_id` = ?", $iObjectId, $iAuthorId, time(), $iListId);
+        return (int)$this->query($sQuery) > 0;
+    }
+    
+    public function undoFavorite($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        if((int)$this->getOne($sQuery) == 0)
+        	return true;
+
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->query($sQuery) > 0;
+    }
+    
+    public function getList($aParams)
+    {
+        $aMethodParams = array();
+        switch($aParams['type']) {
+            case 'object_and_author':
+                $sMethod = 'getColumn';
+                $aMethodParams[0] = $this->prepare("SELECT `list_id` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ?", $aParams['object_id'], $aParams['author_id']);
+                break;
+                
+            case 'id':
+                $sMethod = 'getRow';
+                $aMethodParams[0] = $this->prepare("SELECT * FROM `{$this->_sTableLists}` WHERE `id` = ? ", $aParams['list_id']);
+                break;
+                
+            case 'info':
+                $sMethod = 'getRow';
+                $aMethodParams[0] = $this->prepare("SELECT MAX(`t`.`date`) `updated`, COUNT(`t`.`object_id`) `count`, `l`.`title`, `l`.`date` `created`, `l`.`allow_view_favorite_list_to` FROM `{$this->_sTableTrack}` `t` LEFT JOIN `{$this->_sTableLists}` `l` ON `t`.`list_id` = `l`.`id` WHERE `t`.`list_id` = ? AND `t`.`author_id` = ?", $aParams['list_id'], $aParams['author_id']);
+                break;
+            
+            case 'all':
+                $sMethod = 'getAll';
+                $sQ = "SELECT `id`, `title`, `allow_view_favorite_list_to` FROM `{$this->_sTableLists}` WHERE `author_id` = ? ORDER BY `date`";
+                if (isset($aParams['need_default']) && (bool)$aParams['need_default']){
+                    $sQ = "SELECT 0 `id`, '" . _t('_sys_txt_default_favorite_list') . "' `title`, 3 `allow_view_favorite_list_to` UNION (" . $sQ . ")";
+                }
+                
+                $aMethodParams[0] = $this->prepare($sQ, $aParams['author_id']);
+                break;
+                
+            case 'active':
+                $sMethod = 'getPairs';
+                $aMethodParams[0] = "SELECT `l`.`id`, `l`.`title` FROM `{$this->_sTableLists}` `l` INNER JOIN  `{$this->_sTableTrack}` `t` ON `t`.`list_id` = `l`.`id` WHERE `l`.`author_id` = :author GROUP BY `l`.`id`, `l`.`title` ORDER BY `l`.`date` DESC";
+                $aMethodParams[1] = 'id';
+                $aMethodParams[2] = 'title';
+                
+                $aBindings = array(
+                    'author' => $aParams['author_id'],
+                );
+                
+                if (isset($aParams['need_default']) && (bool)$aParams['need_default'])
+                    $aMethodParams[0] = "SELECT 0 `id`, '" . _t('_sys_txt_default_favorite_list') . "' `title` UNION (" . $aMethodParams[0] . ")";
+                
+                if (isset($aParams['limit'])){
+                    $aMethodParams[0] .= " LIMIT " . $aParams['start'] . ", " . $aParams['limit'];
+                }
+                
+                $aMethodParams[3] = $aBindings;
+                break;
+        }
+        return call_user_func_array(array($this, $sMethod), $aMethodParams);
+    }
+    
+    public function addList($iAuthorId, $sTitle, $sVisibility)
+    {
+        $sQuery = $this->prepare("INSERT INTO `{$this->_sTableLists}` SET `author_id` = ?, `title` = ?, `allow_view_favorite_list_to` = ?, `date` = ?", $iAuthorId, $sTitle, $sVisibility, time());
+        $this->query($sQuery);
+        return $this->lastId();
+    }
+    
+    public function editList($iListId, $sTitle, $sVisibility)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTableLists}` SET `title` = ?, `allow_view_favorite_list_to` = ? WHERE `id` = ?", $sTitle, $sVisibility, $iListId);
+        $this->query($sQuery);
+        return $this->lastId();
+    }
+    
+    public function deleteList($iListId)
+    {
+        $sQuery = $this->prepare("DELETE FROM  `{$this->_sTableLists}` WHERE  `id` = ?", $iListId);
+        $this->query($sQuery);
+        
+        $sQuery = $this->prepare("DELETE FROM  `{$this->_sTableTrack}` WHERE  `list_id` = ?", $iListId);
+        $this->query($sQuery);
+    }
+}
+
+/** @} */

+ 220 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFeature.php

@@ -0,0 +1,220 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_FEATURED_USAGE_BLOCK', 'block');
+define('BX_DOL_FEATURED_USAGE_INLINE', 'inline');
+define('BX_DOL_FEATURED_USAGE_DEFAULT', BX_DOL_FEATURED_USAGE_BLOCK);
+
+/**
+ * Track any object feature automatically
+ *
+ * Add record to sys_object_feature table to track object feature,
+ * to record feature just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolFeature::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_feature table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track feature
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update feature field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where the featuring date is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ */
+class BxDolFeature extends BxDolObject
+{
+    protected $_sBaseUrl;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolFeatureQuery($this);
+
+        $this->_sBaseUrl = BxDolPermalinks::getInstance()->permalink($this->_aSystem['base_url']);
+        if(get_mb_substr($this->_sBaseUrl, 0, 4) != 'http')
+            $this->_sBaseUrl = BX_DOL_URL_ROOT . $this->_sBaseUrl;
+    }
+
+   /**
+     * get feature object instanse
+     * @param $sSys feature object name
+     * @param $iId associated content id, where feature is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        $sKey = 'BxDolFeature!' . $sSys . $iId;
+        if(isset($GLOBALS['bxDolClasses'][$sKey]))
+            return $GLOBALS['bxDolClasses'][$sKey];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplFeature';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit);
+        return ($GLOBALS['bxDolClasses'][$sKey] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        $sKey = 'bx_dol_cache_memory_feature_systems';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_feature', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `module` AS `module`,
+                    `is_on` AS `is_on`,
+                    `is_undo` AS `is_undo`,
+                    `base_url` AS `base_url`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_flag` AS `trigger_field_flag`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_feature`', 'name');
+
+        return $GLOBALS[$sKey];
+    }
+
+	/**
+     * Actions functions
+     */
+    public function actionFeature()
+    {
+        return echoJson($this->_doFeature());
+    }
+
+    public function actionGetFeatureBy()
+    {
+        return '';
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedFeature($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('feature', $isPerformAction);
+    }
+
+	public function msgErrAllowedFeature()
+    {
+        return $this->checkActionErrorMsg('feature');
+    }
+
+    /**
+     * Auxiliary functions
+     */
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+	/**
+     * Internal functions
+     */
+	protected function _doFeature()
+    {
+        if (!$this->isEnabled())
+           return array('code' => 1, 'message' => _t('_feature_err_not_enabled'));
+
+        $iObjectId = $this->getId();
+        $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($iObjectId);
+        $iAuthorId = $this->_getAuthorId();
+
+        $bUndo = $this->isUndo();
+        $bPerformed = $this->isPerformed($iObjectId, $iAuthorId);
+        $bPerformUndo = $bPerformed && $bUndo ? true : false;
+
+        if(!$bPerformUndo && !$this->isAllowedFeature())
+            return array('code' => 2, 'message' => $this->msgErrAllowedFeature());
+
+        if($bPerformed && !$bUndo)
+        	return array('code' => 3, 'message' => _t('_feature_err_duplicate_feature'));
+
+        if(!$this->_triggerValue($bPerformUndo ? 0 : time()))
+            return array('code' => 4, 'message' => _t('_feature_err_cannot_perform_action'));
+            
+        if(!$bPerformUndo)
+            $this->isAllowedFeature(true);
+        
+        bx_audit(
+            $this->getId(), 
+            $this->_aSystem['name'], 
+            '_sys_audit_action_' . ($bPerformUndo ? 'un' : '') . 'feature',  
+            $this->_prepareAuditParams()
+        );
+
+        bx_alert($this->_sSystem, ($bPerformUndo ? 'un' : '') . 'feature', $iObjectId, $iAuthorId, array('object_author_id' => $iObjectAuthorId));
+        bx_alert('feature', ($bPerformUndo ? 'un' : '') . 'do', 0, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId));
+
+        return array(
+        	'eval' => $this->getJsObjectName() . '.onFeature(oData, oElement)',
+        	'code' => 0, 
+        	'label_icon' => $this->_getIconDoFeature(!$bPerformed),
+            'label_title' => _t($this->_getTitleDoFeature(!$bPerformed)),
+            'disabled' => !$bPerformed && !$bUndo
+        );
+    }
+
+    protected function _getIconDoFeature($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? 'far star' : 'star';
+    }
+
+    protected function _getTitleDoFeature($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? '_feature_do_unfeature' : '_feature_do_feature';
+    }
+    
+    private function _prepareAuditParams()
+    {
+        $sModule = $this->_aSystem['module'];
+        $oModule = BxDolModule::getInstance($sModule);
+        if(!$oModule)
+            return;
+
+        $CNF = $oModule->_oConfig->CNF;
+
+        $aContentInfo = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $this->getId()))) : array();
+        
+        $AuditParams = array(
+            'content_title' => isset($CNF['FIELD_TITLE'])  ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+            'content_info_object' =>  isset($CNF['OBJECT_CONTENT_INFO']) ? $CNF['OBJECT_CONTENT_INFO'] : '',
+        );
+        
+        return $AuditParams;
+    }
+}
+
+/** @} */

+ 1894 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolForm.php

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

+ 44 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormNested.php

@@ -0,0 +1,44 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Nested form. It is used in file uploaders to show uploaded file as nested form.
+ * @see BxDolUploader
+ * @see BxDolForm
+ */
+class BxDolFormNested extends BxTemplFormView
+{
+    public function __construct ($aInfo, $oTemplate)
+    {
+        parent::__construct($aInfo, $oTemplate);
+    }
+    
+    function genInputStandard(&$aInput)
+    {
+        if($aInput['type'] != 'checkbox')
+            return parent::genInputStandard($aInput);
+
+        $aInputHidden = $aInput;
+        
+        $aInputHidden['type'] = 'hidden';
+        if (isset($aInput['checked']) and $aInput['checked'])
+            $aInputHidden['value'] = '1';
+        else
+            $aInputHidden['value'] = '0';
+        
+        $sRv = parent::genInputStandard($aInputHidden);
+        $aInput['name'] = 'chk_' . $aInput['name'];
+        $aInput['attrs']['onchange'] = 'BxDolForm.setCheckBoxValue(this)';
+        $sRv .= parent::genInputStandard($aInput);
+   
+        return $sRv;
+    }
+}
+
+/** @} */

+ 56 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormNestedGhost.php

@@ -0,0 +1,56 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Nested form. It is used in file uploaders to show uploaded file as nested form.
+ * @see BxDolUploader
+ * @see BxDolForm
+ */
+class BxDolFormNestedGhost 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);
+    }
+}
+
+/** @} */

+ 353 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolFormQuery.php

@@ -0,0 +1,353 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolForm');
+
+/**
+ * Database queries for forms.
+ * @see BxDolForm
+ */
+class BxDolFormQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    protected static $TYPES_SET = array (
+        'select_multiple' => 1,
+        'checkbox_set' => 1,
+    );
+    protected static $TYPES_TRANSLATABLE = array (
+        'submit' => 1,
+        'reset' => 1,
+        'button' => 1,
+    );
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getFormObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_form` WHERE `object` = ?", $sObject);
+        return $oDb->fromMemory('sys_objects_form_' . $sObject, 'getRow', $sQuery);
+    }
+    
+    static public function getNestedFormObjects ($sParentObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_form` WHERE `parent_form` = ?", $sParentObject);
+        return $oDb->fromMemory('sys_objects_form_' . $sParentObject . '_nested', 'getAll', $sQuery);
+    }
+    
+    static public function deleteDataFromNestedForm ($sTableName, $iContentId)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("DELETE FROM  `" . $sTableName ."`  WHERE `content_id` = ?", $iContentId);
+        $oDb->query($sQuery);
+    }
+    
+    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->fromMemory('sys_objects_form_' . $sObject, '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->fromMemory('sys_form_displays_' . $sObject . '_' . $sDisplayName, 'getRow', $sQuery);
+        if (!$aDisplay || !is_array($aDisplay))
+            return false;
+
+        $aForm = array ();
+
+        // form attrs
+        if (!empty($aObject['form_attrs']))
+            $aAddFormAttrs = unserialize($aObject['form_attrs']);
+
+        $aDefaultsFormAttrs = array(
+            'action' => $aObject['action'],
+            'name' => $aObject['object'],
+            'id' => $aObject['object'],
+        );
+
+        $aForm['form_attrs'] = array_merge($aDefaultsFormAttrs, !empty($aAddFormAttrs) && is_array($aAddFormAttrs) ? $aAddFormAttrs : array());
+
+        // form action
+        if (!empty($aForm['form_attrs']['action']) && 0 !== strncasecmp($aForm['form_attrs']['action'], 'http://', 7) && 0 !== strncasecmp($aForm['form_attrs']['action'], 'https://', 8))
+            $aForm['form_attrs']['action'] = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($aForm['form_attrs']['action']);
+
+        // params
+        if (!empty($aObject['params']))
+            $aAddFormParams = unserialize($aObject['params']);
+
+        $mixedSubminName = '';
+        if (!empty($aObject['submit_name'])) {
+            $mixedSubminName = @unserialize($aObject['submit_name']);
+            if($mixedSubminName === false)
+                $mixedSubminName = $aObject['submit_name'];
+        }
+
+        $aDefaultsFormParams = array(
+            'db' => array(
+                'submit_name' => $mixedSubminName,
+                'table' => $aObject['table'],
+                'key' => $aObject['key'],
+                'uri' => $aObject['uri'],
+                'uri_title' => $aObject['uri_title'],
+            ),
+            'object' => $sObject,
+            'display' => $sDisplayName,
+            'view_mode' => $aDisplay['view_mode'],
+        );
+
+        $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 (
+                'id' => $a['id'],
+                'type' => $a['type'],
+                'name' => $a['name'],
+                'caption_system_src' => $a['caption_system'],
+            	'caption_src' => $a['caption'],
+                'caption' => _t($a['caption']),
+            	'info_src' => $a['info'] ? $a['info'] : '',
+                'info' => $a['info'] ? _t($a['info']) : '',
+                'required' => $a['required'] ? true : false,
+            	'unique' => $a['unique'] ? true : false,
+                'collapsed' => $a['collapsed'] ? true : false,
+                'privacy' => $a['privacy'] ? true : false,
+                'rateable' => $a['rateable'],
+                'html' => $a['html'],
+                'attrs' => $a['attrs'] ? unserialize($a['attrs']) : false,
+                'tr_attrs' => $a['attrs_tr'] ? unserialize($a['attrs_tr']) : false,
+                'attrs_wrapper' => $a['attrs_wrapper'] ? unserialize($a['attrs_wrapper']) : false,
+                'visible_for_levels' => $a['visible_for_levels'],
+            );
+
+            // if type is input set stop to process other attribnutes
+            if ('input_set' == $a['type']) {
+                $aForm['inputs'][$a['name']] = $aInput;
+                $aInputSets[] = $a['name'];
+                continue;
+            }
+
+            // default value
+            if (!empty($a['value']))
+                $aInput['value'] = isset(self::$TYPES_TRANSLATABLE[$aInput['type']]) ? _t($a['value']) : $a['value'];
+
+            if (!empty($a['values'])) {
+                $aInput['values_src'] = $a['values'];
+
+                if (0 === strncmp(BX_DATA_LISTS_KEY_PREFIX, $a['values'], 2)) {
+                    $aInput['values_list_name'] = trim($a['values'], BX_DATA_LISTS_KEY_PREFIX . ' ');
+                    $aInput['values'] = self::getDataItems(trim($a['values'], BX_DATA_LISTS_KEY_PREFIX . ' '), isset(self::$TYPES_SET[$aInput['type']]));
+                } else {
+                    $aInput['values'] = unserialize($a['values']);
+                }
+            }
+
+            $aInput['checked'] = $a['checked'] ? true : false;
+
+            // checker function options
+            if (!empty($a['checker_func'])) {
+                $aChecker = array ('func' => $a['checker_func']);
+                if ($a['checker_error'])
+                    $aChecker['error'] = _t($a['checker_error']);
+                if ($a['checker_params'])
+                    $aChecker['params'] = unserialize($a['checker_params']);
+                $aInput['checker'] = $aChecker;
+            }
+
+            // db processing options
+            if (!empty($a['db_pass'])) {
+                $aDb = array ('pass' => $a['db_pass']);
+                if ($a['db_params'])
+                    $a['params'] = unserialize($a['db_params']);
+                $aInput['db'] = $aDb;
+            }
+
+            $aForm['inputs'][$a['name']] = $aInput;
+        }
+
+        foreach ($aInputSets as $sName) {
+            $a = explode(',', $aInputs[$sName]['values']);
+            foreach ($a as $sInputName) {
+                if (!isset($aForm['inputs'][$sInputName]))
+                    continue;
+                $aForm['inputs'][$sName][] = $aForm['inputs'][$sInputName];
+                unset($aForm['inputs'][$sInputName]);
+            }
+        }
+
+        $aForm['override_class_name'] = $aObject['override_class_name'];
+        $aForm['override_class_file'] = $aObject['override_class_file'];
+
+        return $aForm;
+    }
+
+    static public function getDataItems($sKey, $isUseForSet = false, $sUseValues = BX_DATA_VALUES_DEFAULT)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        if(!in_array($sUseValues, array(BX_DATA_VALUES_DEFAULT, BX_DATA_VALUES_ADDITIONAL, BX_DATA_VALUES_ALL)))
+            $sUseValues = BX_DATA_VALUES_DEFAULT;
+
+        $sQuery = $oDb->prepare("SELECT `Value`, `LKey`, `LKey2`, `Data` FROM `sys_form_pre_values` WHERE `Key` = ? ORDER BY `Order` ASC", $sKey);
+        if($sUseValues == BX_DATA_VALUES_ALL)
+            return $oDb->getAllWithKey($sQuery, 'Value');
+
+        $a = $oDb->getAll($sQuery);
+
+        $iMaxValue = 0;
+        if ($isUseForSet)
+            $iMaxValue = log(BX_DOL_INT_MAX, 2) + 1;
+
+        $aRet = array();
+        foreach ($a as $r) {
+            if ($isUseForSet && (!is_numeric($r['Value']) || $r['Value'] < 1 || $r['Value'] > $iMaxValue))
+                continue;
+            $aRet[$r['Value']] = BX_DATA_VALUES_ADDITIONAL == $sUseValues && !empty($r[BX_DATA_VALUES_ADDITIONAL]) ? _t($r[BX_DATA_VALUES_ADDITIONAL]) : _t($r[BX_DATA_VALUES_DEFAULT]);
+        }
+
+        return $aRet;
+    }
+
+    static public function fieldCheckUnique($sTable, $sField, $sValue)
+    {
+        return uriCheckUniq($sValue, $sTable, $sField);
+    } 
+
+    static public function fieldGetValue($sTable, $sField, $sFieldKey, $sFieldKeyValue)
+    {
+        return BxDolDb::getInstance()->getOne("SELECT `" . $sField . "` FROM `" . $sTable . "` WHERE `" . $sFieldKey . "`=:field_key LIMIT 1", array(
+            'field_key' => $sFieldKeyValue
+        ));
+    }
+
+    static public function getInputByName($sObject, $sName)
+    {
+        return BxDolDb::getInstance()->getRow("SELECT * FROM `sys_form_inputs` WHERE `object`=:object AND `name`=:name LIMIT 1", array(
+            'object' => $sObject, 
+            'name' => $sName
+        ));
+    }
+
+    static public function getInputPrivacy($iInputId, $iAuthorId, $sPrivacyField = '')
+    {
+        $sMethod = 'getRow';
+        $sSelectClause = '*';
+
+        if(!empty($sPrivacyField)) {
+            $sMethod = 'getOne';
+            $sSelectClause = '`' . $sPrivacyField . '`';
+        }
+
+        return BxDolDb::getInstance()->$sMethod("SELECT " . $sSelectClause . " FROM `sys_form_inputs_privacy` WHERE `input_id`=:input_id AND `author_id`=:author_id LIMIT 1", array(
+            'input_id' => $iInputId,
+            'author_id' => $iAuthorId,
+        ));
+    }
+
+    static public function setInputPrivacy($iInputId, $iAuthorId, $sPrivacyField, $sPrivacyValue)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sTable = 'sys_form_inputs_privacy';
+        $aBindingsSet = array(
+            $sPrivacyField => $sPrivacyValue
+        );
+        $aBindingsWhere = array(
+            'input_id' => $iInputId,
+            'author_id' => $iAuthorId,
+        );
+
+        $bResult = false;
+        if((int)$oDb->getOne("SELECT `id` FROM `" . $sTable . "` WHERE `input_id`=:input_id AND `author_id`=:author_id LIMIT 1", $aBindingsWhere) != 0)
+            $bResult = $oDb->query("UPDATE `" . $sTable . "` SET " . $oDb->arrayToSQL($aBindingsSet) . " WHERE " . $oDb->arrayToSQL($aBindingsWhere, ' AND ')) !== false;
+        else
+            $bResult = (int)$oDb->query("INSERT `" . $sTable . "` SET " . $oDb->arrayToSQL(array_merge($aBindingsSet, $aBindingsWhere))) > 0;
+
+        return $bResult;
+    }
+    
+    static public function addFormField($sObjectForm, $sFieldName, $iContentId, $iAuthorId, $sModuleName, $iNestedContentId = 0)
+    {
+        $oDb = BxDolDb::getInstance();
+        $aBindings = array(
+            'object_form' => $sObjectForm,
+            'field_name' => $sFieldName,
+            'content_id' => $iContentId,
+            'author_id' => $iAuthorId,
+            'module' => $sModuleName,
+            'nested_content_id' => $iNestedContentId
+        );
+        $oDb->query("INSERT `sys_form_fields_ids` SET `object_form`=:object_form, `field_name`=:field_name, `content_id`=:content_id, `author_id`=:author_id, `module`=:module, nested_content_id=:nested_content_id", $aBindings);
+    }
+    
+    static public function getFormField($sObjectForm, $sFieldName, $iContentId, $iNestedContentId = 0)
+    {
+        $oDb = BxDolDb::getInstance();
+        $aBindings = array(
+            'object_form' => $sObjectForm,
+            'field_name' => $sFieldName,
+            'content_id' => $iContentId,
+            'nested_content_id' => $iNestedContentId
+        );
+        return $oDb->getOne("SELECT `id` FROM `sys_form_fields_ids` WHERE `object_form`=:object_form AND `field_name`=:field_name AND `content_id`=:content_id AND nested_content_id=:nested_content_id", $aBindings);
+    }
+    
+    static public function removeFormField($sObjectForm, $iContentId = 0)
+    {
+        $oDb = BxDolDb::getInstance();
+        if ($iContentId > 0){
+            $aBindings = array(
+                'object_form' => $sObjectForm,
+                'content_id' => $iContentId
+            );
+            $oDb->query("DELETE FROM `sys_form_fields_ids` WHERE  `object_form` = :object_form AND `content_id` = :content_id", $aBindings);
+        }
+        else{
+            $aBindings = array(
+                'object_form' => $sObjectForm
+            );
+            $oDb->query("DELETE FROM `sys_form_fields_ids` WHERE  `object_form` = :object_form", $aBindings);
+        }
+    }
+        
+    static public function removeFormFields($sModule)
+    {
+        $oDb = BxDolDb::getInstance();
+        $aBindings = array(
+            'module' => $sModule,
+        );
+        $oDb->query("DELETE FROM `sys_form_fields_ids` WHERE  `module` = :module", $aBindings);
+    }
+
+    public function getFormInputs()
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_form_inputs` WHERE `object` = ? ORDER BY `order` ASC", $this->_aObject['object']);
+        return $this->getAll($sQuery);
+    }
+}
+
+/** @} */

+ 587 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolGrid.php

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

+ 185 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolGridConnections.php

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

+ 124 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLabel.php

@@ -0,0 +1,124 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLabel extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oDb;
+
+    protected $_sForm;
+    protected $_sFormDisplaySelect;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolLabelQuery();
+
+        $this->_sForm= 'sys_labels';
+        $this->_sFormDisplaySelect = 'sys_labels_select';
+    }
+
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    public static function getInstance($oTemplate = false)
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplLabel($oTemplate);
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function actionSelectLabels()
+    {
+        return echoJson($this->selectLabels(array(
+            'name' => bx_process_input(bx_get('name')),
+            'list' => bx_process_input(bx_get('value'))
+        )));
+    }
+
+    public function actionLabelsList()
+    {
+        $sTerm = bx_process_input(bx_get('term'));
+
+        $aLabels = $this->getLabels(array('type' => 'term', 'term' => $sTerm));
+
+        $aResult = array();
+        foreach($aLabels as $aLabel)
+            $aResult[] = array (
+            	'label' => $aLabel['value'], 
+                'value' => $aLabel['value'], 
+            );
+
+        echoJson($aResult);
+    }
+
+    public function getLabels($aParams = array())
+    {
+        return $this->_oDb->getLabels($aParams);
+    }
+
+    public function getLabelUrl($sKeyword, $mixedSection = false) 
+    {   
+        $sSectionPart = '';
+        if(!empty($mixedSection)) {
+            if (is_array($mixedSection))
+                $sSectionPart = '&section[]=' . implode('&section[]=', $mixedSection);
+            elseif (is_string($mixedSection))
+                $sSectionPart = '&section[]=' . $mixedSection;
+        }
+
+        $sUrl = BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . $sSectionPart;
+
+        bx_alert('meta_keyword', 'url', 0, false, array(
+           'url' => &$sUrl,
+           'keyword' => $sKeyword,
+           'section' => $mixedSection,
+        ));
+
+        return $sUrl;
+    }
+
+    public function getFormFieldId($aInput = array())
+    {
+        return !empty($aInput['name']) ? $this->_aHtmlIds['labels_element'] . $aInput['name'] : 'bx-form-element-labels';
+    }
+
+    public function onAdd($iId)
+    {
+        $aLabel = $this->_oDb->getLabels(array('type' => 'id', 'id' => $iId));
+        if(empty($aLabel) || !is_array($aLabel))
+            return;
+
+        bx_alert('label', 'added', $iId, false, array('label' => $aLabel));
+    }
+
+    public function onEdit($iId)
+    {
+        $aLabel = $this->_oDb->getLabels(array('type' => 'id', 'id' => $iId));
+        if(empty($aLabel) || !is_array($aLabel))
+            return;
+
+        bx_alert('label', 'edited', $iId, false, array('label' => $aLabel));
+    }
+
+    public function onDelete($iId)
+    {
+        $aLabel = $this->_oDb->getLabels(array('type' => 'id', 'id' => $iId));
+        if(empty($aLabel) || !is_array($aLabel))
+            return;
+
+        bx_alert('label', 'deleted', $iId, false, array('label' => $aLabel));
+    }
+}
+
+/** @} */

+ 106 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLabelQuery.php

@@ -0,0 +1,106 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolLabel
+ */
+class BxDolLabelQuery extends BxDolDb
+{
+    protected $_sTableLabels;
+
+    public function __construct()
+    {
+    	parent::__construct();
+
+        $this->_sTableLabels = 'sys_labels';
+    }
+
+    public function getLabels($aParams = array())
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`tl`.*";
+        $sJoinClause = $sWhereClause = $sGroupClause = "";
+        $sOrderClause = "`tl`.`order` ASC";
+
+        if(isset($aParams['count_only']) && $aParams['count_only'] === true) {
+            $aMethod['name'] = 'getOne';
+            $sSelectClause = "COUNT(`tl`.`id`)";
+        }
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause = " AND `tl`.`id`=:id";
+                break;
+
+            case 'value':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'value' => mb_strtoupper($aParams['value'])
+                );
+
+                $sWhereClause = " AND UPPER(`tl`.`value`)=:value";
+                break;
+
+            case 'values':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`tl`.`value`";
+                break;
+
+            case 'parent':
+                $aMethod['params'][1] = array(
+                    'parent' => $aParams['parent']
+                );
+
+                $sWhereClause = " AND `tl`.`parent`=:parent";
+
+                if(!empty($aParams['exclude']) && is_array($aParams['exclude']))
+                    $sWhereClause .= " AND `tl`.`id` NOT IN(" . $this->implode_escape($aParams['exclude']) . ")";
+                break;
+
+            case 'term':
+                $aMethod['params'][1] = array(
+                    'term' => '%' . $aParams['term'] . '%'
+                );
+
+                $sWhereClause = " AND `tl`.`value` LIKE :term";
+                break;
+
+            case 'parent_order':
+                $aMethod['name'] = 'getOne';
+                $aMethod['params'][1] = array(
+                    'parent' => $aParams['parent']
+                );
+
+                $sSelectClause = "MAX(`tl`.`order`)";
+                $sWhereClause = " AND `tl`.`parent`=:parent";
+                break;
+        }
+
+        if(!empty($sGroupClause))
+            $sGroupClause = "GROUP BY " . $sGroupClause;
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " 
+            FROM `" . $this->_sTableLabels . "` AS `tl`" . $sJoinClause . " 
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }   
+}
+
+/** @} */

+ 316 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLiveUpdates.php

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

+ 31 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLiveUpdatesQuery.php

@@ -0,0 +1,31 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolLiveUpdates
+ */
+class BxDolLiveUpdatesQuery extends BxDolDb
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function getSystems()
+    {
+        $sKey = 'bx_dol_cache_memory_live_updates_systems';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_live_updates', 'getAllWithKey', 'SELECT * FROM `sys_objects_live_updates`', 'name');
+
+        return $GLOBALS[$sKey];
+    }
+}
+
+/** @} */

+ 55 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationField.php

@@ -0,0 +1,55 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * This class allow different implementations for location field.
+ *
+ * Add record to sys_objects_location_field table, like you are doing this for Comments or Voting objects:
+ * - object: your object name, usually it is in the following format - vendor prefix, underscore, module prefix;
+ * - module: module name
+ * - title: translatable title
+ * - class_name: user defined class name which is derived from BxDolLocationField.
+ * - class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ */
+class BxDolLocationField extends BxDolFactoryObject
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject, null, 'BxDolLocationFieldQuery');
+    }
+
+   /**
+     * Get object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject)
+    {
+        return parent::getObjectInstanceByClassNames($sObject, null, 'BxDolLocationField', 'BxDolLocationFieldQuery');
+    }
+
+    public function genInputLocation (&$aInput, $oForm)
+    {        
+        // override this
+        return 'not implemented';
+    }
+
+    protected function getLocationVal ($aInput, $sIndex, $oForm) 
+    {
+        $aSpecificValues = $oForm->getSpecificValues();
+
+        $s = $aInput['name'] . '_' . $sIndex;
+        if (isset($aSpecificValues[$s]))
+            return $aSpecificValues[$s];
+
+        return $oForm->getCleanValue($s);
+    }
+}
+
+/** @} */

+ 76 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldGoogle.php

@@ -0,0 +1,76 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLocationFieldGoogle extends BxDolLocationField
+{
+    public function genInputLocation (&$aInput, $oForm)
+    {
+        $aVars = $this->_getInputLocationVars($aInput, $oForm);
+        return $oForm->getTemplate()->parseHtmlByName('location_field_google.html', $aVars);
+    }
+
+    protected function _getInputLocationVars(&$aInput, $oForm)
+    {
+        $isManualInput = (int)(isset($aInput['manual_input']) && $aInput['manual_input']);
+        $sIdStatus = $oForm->getInputId($aInput) . '_status';
+        $sIdInput = $oForm->getInputId($aInput) . '_location';
+
+        $aVars = array (
+            'key' => trim(getParam('sys_maps_api_key')),
+            'lang' => bx_lang_name(),
+            'name' => $aInput['name'],
+            'id_status' => $sIdStatus,
+            'id_input' => $sIdInput,
+            'manual_input' => $isManualInput,            
+            'bx_if:manual_input' => array(
+                'condition' => $isManualInput,
+                'content' => array(),
+            ),
+            'bx_if:auto_input' => array(
+                'condition' => !$isManualInput,
+                'content' => array(
+                    'id_status' => $sIdStatus,
+                    'location_string' => _t('_sys_location_undefined'),
+                ),
+            ),
+            'api_field_name_short' => 'short_name',
+            'api_field_name_long' => 'long_name',
+            'api_field_name_2_length' => json_encode(array()),
+        );
+
+        $aLocationIndexes = BxDolForm::$LOCATION_INDEXES;
+        foreach ($aLocationIndexes as $sKey)
+            $aVars[$sKey] = $this->getLocationVal($aInput, $sKey, $oForm);
+
+        if ($aVars['country']) {
+            $aCountries = BxDolFormQuery::getDataItems('Country');
+            $s = ($aVars['street_number'] ? $aVars['street_number'] . ', ' : '') . ($aVars['street'] ? $aVars['street'] . ', ' : '') . ($aVars['city'] ? $aVars['city'] . ', ' : '') . ($aVars['state'] ? $aVars['state'] . ', ' : '') . $aCountries[$aVars['country']];
+            $aVars['bx_if:auto_input']['content']['location_string'] = $s;
+        }
+
+        if ($isManualInput) {
+            $aAttrs = empty($aInput['attrs']) ? array() : $aInput['attrs'];
+			$aInput['type'] = 'text';
+            $aInput['attrs']['id'] = $sIdInput;
+			$aInput['attrs'] = array_merge($aAttrs, $aInput['attrs']);
+            $aVars['input'] = $oForm->genInputStandard($aInput);
+        } 
+        else {
+            if ($this->getLocationVal($aInput, 'lat', $oForm) && $this->getLocationVal($aInput, 'lng', $oForm))
+                $aInput['checked'] = true;
+            else
+                $aInput['checked'] = $oForm->getCleanValue($aInput['name'] . '_lat') && $oForm->getCleanValue($aInput['name'] . '_lng') ? 1 : 0;
+            $aVars['input'] = $oForm->genInputSwitcher($aInput);
+        }
+
+        return $aVars;
+    }
+}
+
+/** @} */

+ 105 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldNominatim.php

@@ -0,0 +1,105 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Plain fields with Nomination service geocoding
+ */
+class BxDolLocationFieldNominatim extends BxDolLocationField
+{
+    public function genInputLocation (&$aInput, $oForm)
+    {        
+        $isManualInput = (int)(isset($aInput['manual_input']) && $aInput['manual_input']);
+        if (!$isManualInput) {
+            $aVars = array();
+            $aInputField = $aInput;
+
+            $aLocationIndexes = BxDolForm::$LOCATION_INDEXES;
+            foreach ($aLocationIndexes as $sKey)
+                $aVars[$sKey] = $this->getLocationVal($aInput, $sKey, $oForm);
+
+            $aInputField['caption'] = _t('_sys_location_undefined');
+
+            if ($this->getLocationVal($aInput, 'lat', $oForm) && $this->getLocationVal($aInput, 'lng', $oForm))
+                $aInputField['checked'] = true;
+            else
+                $aInputField['checked'] = $oForm->getCleanValue($aInput['name'] . '_lat') && $oForm->getCleanValue($aInput['name'] . '_lng') ? 1 : 0;
+
+            $sLocationString = _t('_sys_location_undefined');
+            if ($aVars['country']) {
+                $aCountries = BxDolFormQuery::getDataItems('Country');
+                $sLocationString = ($aVars['street_number'] ? $aVars['street_number'] . ', ' : '') . ($aVars['street'] ? $aVars['street'] . ', ' : '') . ($aVars['city'] ? $aVars['city'] . ', ' : '') . ($aVars['state'] ? $aVars['state'] . ', ' : '') . $aCountries[$aVars['country']];
+            }
+            elseif ($aVars['lat'] || $aVars['lng']) {
+                $sLocationString = $aVars['lat'] . ', ' . $aVars['lng'];
+            }
+
+            $aVars['name'] = $aInput['name'];
+            $aVars['input'] = $oForm->genInputSwitcher($aInputField);
+            $aVars['id_status'] = $oForm->getInputId($aInput) . '_status';
+            $aVars['location_string'] = $sLocationString;
+            $aVars['nominatim_server'] = $this->getNominatimServer();
+            $aVars['nominatim_email'] = $this->getNominatimEmail();
+
+            $sRet = $oForm->getTemplate()->parseHtmlByName('location_field_plain_auto.html', $aVars);
+        }
+        else {
+            $aFields = array(
+                'lat' => array('type' => 'hidden'),
+                'lng' => array('type' => 'hidden'),
+                'street_number' => array('type' => 'text', 'ph' => _t('_sys_location_ph_number')),
+                'street' => array('type' => 'text', 'ph' => _t('_sys_location_ph_street')),
+                'city' => array('type' => 'text', 'ph' => _t('_sys_location_ph_city')),
+                'state' => array('type' => 'text', 'ph' => _t('_sys_location_ph_state')),
+                'zip' => array('type' => 'text', 'ph' => _t('_sys_location_ph_zip')),
+                'country' => array('type' => 'select'),
+            );
+
+            $sInputs = '';
+            foreach ($aFields as $sKey => $a) {
+                $aInputField = $aInput;
+                $aInputField['name'] = $aInput['name'] . '_' . $sKey;
+                $aInputField['type'] = $a['type'];
+                $aInputField['value'] = $this->getLocationVal($aInput, $sKey, $oForm);            
+                $aInputField['attrs']['placeholder'] = empty($a['ph']) ? '' : _t($a['ph']);
+                if (isset($aInputField['attrs']['class']))
+                    $aInputField['attrs']['class'] .= ' bx-form-input-location-' . $sKey;
+                else
+                    $aInputField['attrs']['class'] = 'bx-form-input-location-' . $sKey;
+                if ('country' == $sKey) {
+                    $aCountries = BxDolFormQuery::getDataItems('Country');
+                    array_unshift($aCountries, array('key' => '', 'value' => _t('_None')));
+                    $aInputField['values'] = $aCountries;
+                    $sInputs .= $oForm->genInputSelect($aInputField);
+                }
+                else {
+                    $sInputs .= $oForm->genInputStandard($aInputField);
+                }
+            }
+            $sRet = $oForm->getTemplate()->parseHtmlByName('location_field_plain.html', array(
+                'name' => $aInput['name'],
+                'nominatim_server' => $this->getNominatimServer(),
+                'nominatim_email' => $this->getNominatimEmail(),
+                'inputs' => $sInputs,
+            ));
+        }
+        return $sRet;
+    }
+
+    protected function getNominatimServer ()
+    {
+        return trim(trim(getParam('sys_nominatim_server')), '/');
+    }
+
+    protected function getNominatimEmail ()
+    {
+        return trim(getParam('sys_nominatim_email'));
+    }
+}
+
+/** @} */

+ 23 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationFieldQuery.php

@@ -0,0 +1,23 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLocationFieldQuery extends BxDolFactoryObjectQuery
+{
+    static public function getObject($sObject)
+    {
+        return parent::getObjectFromTable($sObject, 'sys_objects_location_field');
+    }
+
+    static public function getObjects ()
+    {
+        return parent::getObjectsFromTable('sys_objects_location_field');
+    }
+}
+
+/** @} */

+ 55 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMap.php

@@ -0,0 +1,55 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * This class allow different implementations for displaying maps.
+ *
+ * Add record to sys_objects_location_map table, like you are doing this for Comments or Voting objects:
+ * - object: your object name, usually it is in the following format - vendor prefix, underscore, module prefix;
+ * - module: module name
+ * - title: translatable title
+ * - class_name: user defined class name which is derived from BxDolLocationField.
+ * - class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ */
+class BxDolLocationMap extends BxDolFactoryObject
+{
+    protected function __construct($aObject, $oTemplate = null)
+    {
+        parent::__construct($aObject, $oTemplate, 'BxDolLocationFieldQuery');
+    }
+
+   /**
+     * Get object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject, $oTemplate = null)
+    {
+        return parent::getObjectInstanceByClassNames($sObject, $oTemplate, 'BxDolLocationMap', 'BxDolLocationMapQuery');
+    }
+
+    /**
+     * Get location map for single address
+     * @param $aLocation location array with the following indexes: 
+     *          lat, lng, country, state, city, zip, street, street_number
+     * @param $sLocationHtml formatted address string
+     * @param $aParams some specific params
+     */
+    public function getMapSingle($aLocation, $sLocationHtml = '', $aParams = array())
+    {
+        // override this method
+        return '';
+    }
+
+    public function addCssJs()
+    {
+    }
+}
+
+/** @} */

+ 55 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapGoogleStatic.php

@@ -0,0 +1,55 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLocationMapGoogleStatic extends BxDolLocationMap
+{
+    /**
+     * Get location map for single address
+     * @param $aLocation location array with the following indexes: 
+     *          lat, lng, country, state, city, zip, street, street_number
+     * @param $sLocationHtml formatted address string
+     * @param $aParams some specific params
+     */
+    public function getMapSingle($aLocation, $sLocationHtml = '', $aParams = array())
+    {
+        $sMapSize = '1000x144';
+        if (!empty($aParams['location_map_google_static_size']))
+            $sMapSize = $aParams['location_map_google_static_size'];
+
+        $sZoom = (int)getParam('sys_location_map_zoom_default');
+        if (!$sZoom)
+            $sZoom = '7';
+        if (!empty($aParams['location_map_google_static_zoom']))
+            $sZoom = $aParams['location_map_google_static_zoom'];
+
+        $sMapType = 'roadmap';    
+        if (!empty($aParams['location_map_google_static_maptype']))
+            $sMapType = $aParams['location_map_google_static_maptype'];
+
+		$sMapKey = trim(getParam('sys_maps_api_key'));
+        $sLocationEncoded = rawurlencode(trim(strip_tags($sLocationHtml)));
+        $iScale = isset($_COOKIE['devicePixelRatio']) && (int)$_COOKIE['devicePixelRatio'] >= 2 ? 2 : 1;
+        $sLang = bx_lang_name();
+
+        $this->addCssJs();
+
+        return $this->_oTemplate->parseHtmlByName('location_map_google_static.html', array (
+            'map_img' => bx_proto() . '://maps.googleapis.com/maps/api/staticmap?center=' . $sLocationEncoded . '&zoom=' . $sZoom . '&size=' . $sMapSize . '&maptype=' . $sMapType . '&markers=size:small%7C' . $sLocationEncoded . '&scale=' . $iScale . '&language=' . $sLang  . ($sMapKey ? '&key=' . $sMapKey : ''),
+            'location_string' => $sLocationHtml,
+        ));
+    }
+
+    public function addCssJs()
+    {
+        parent::addCssJs();
+        $this->_oTemplate->addCss('location_map_google_static.css');
+    }
+}
+
+/** @} */

+ 103 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapLeaflet.php

@@ -0,0 +1,103 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLocationMapLeaflet extends BxDolLocationMap
+{
+    /**
+     * Get location map for single address
+     * @param $aLocation location array with the following indexes: 
+     *          lat, lng, country, state, city, zip, street, street_number
+     * @param $sLocationHtml formatted address string
+     * @param $aParams some specific params
+     */
+    public function getMapSingle($aLocation, $sLocationHtml = '', $aParams = array())
+    {
+        $sZoom = (int)getParam('sys_location_map_zoom_default');
+        if (!$sZoom)
+            $sZoom = '7';
+        if (!empty($aParams['location_leaflet_zoom']))
+            $sZoom = $aParams['location_leaflet_zoom'];
+
+        $sProvider = getParam('sys_location_leaflet_provider');
+        if (!empty($aParams['location_leaflet_provider']))
+            $sProvider = $aParams['location_leaflet_provider'];
+
+        $this->addCssJs();
+    
+        return $this->_oTemplate->parseHtmlByName('location_map_leaflet.html', array_merge($aLocation, array(
+            'id' => time().rand(0, PHP_INT_MAX),
+            'zoom' => $sZoom,
+            'provider' => $sProvider,
+            'location_string' => $sLocationHtml,
+        )));
+    }
+
+    public function addCssJs()
+    {
+        parent::addCssJs();
+        $this->_oTemplate->addCss(array(
+            'location_map_leaflet.css',
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'leaflet/|leaflet.css',
+        ));
+        $this->_oTemplate->addJs(array(
+            'leaflet/leaflet.js',
+            'leaflet-providers/leaflet-providers.js',
+        ));
+    }
+
+    static public function getProviders()
+    {
+        $a = array(
+            'OpenStreetMap.Mapnik',
+            'OpenStreetMap.DE',
+            'OpenStreetMap.CH',
+            'OpenStreetMap.France',
+            'OpenStreetMap.HOT',
+            'OpenStreetMap.BZH',
+            'OpenTopoMap',
+            'Stadia.AlidadeSmooth',
+            'Stadia.AlidadeSmoothDark',
+            'Stadia.OSMBright',
+            'Stadia.Outdoors',
+            'CyclOSM',
+            'Stamen.Toner',
+            'Stamen.TonerBackground',
+            'Stamen.TonerLite',
+            'Stamen.Watercolor',
+            'Stamen.Terrain',
+            'Stamen.TerrainBackground',
+            'Esri.WorldStreetMap',
+            'Esri.DeLorme',
+            'Esri.WorldTopoMap',
+            'Esri.WorldImagery',
+            'Esri.WorldTerrain',
+            'Esri.WorldShadedRelief',
+            'Esri.WorldPhysical',
+            'Esri.OceanBasemap',
+            'Esri.NatGeoWorldMap',
+            'Esri.WorldGrayCanvas',
+            'MtbMap',
+            'CartoDB.Positron',
+            'CartoDB.PositronNoLabels',
+            'CartoDB.DarkMatter',
+            'CartoDB.DarkMatterNoLabels',
+            'CartoDB.Voyager',
+            'CartoDB.VoyagerNoLabels',
+            'CartoDB.VoyagerLabelsUnder',
+            'HikeBike.HikeBike',
+            'HikeBike.HillShading',
+            'USGS.USTopo',
+            'USGS.USImagery',
+            'USGS.USImageryTopo',
+        );
+        return array_combine($a, $a);
+    }
+}
+
+/** @} */

+ 23 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLocationMapQuery.php

@@ -0,0 +1,23 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLocationMapQuery extends BxDolFactoryObjectQuery
+{
+    static public function getObject($sObject)
+    {
+        return parent::getObjectFromTable($sObject, 'sys_objects_location_map');
+    }
+
+    static public function getObjects ()
+    {
+        return parent::getObjectsFromTable('sys_objects_location_map');
+    }
+}
+
+/** @} */

+ 149 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogs.php

@@ -0,0 +1,149 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * This class unifies the usage of logs.
+ *
+ * Add record to sys_objects_logs table, like you are doing this for Comments or Voting objects:
+ * - object: your logs object name, usually it is in the following format - vendor prefix, underscore, module prefix;
+ * - module: module name
+ * - logs_storage: logs storage, supported storages: 
+ *      - `Auto`: log to the storage currently configured in settings
+ *      - `Folder`: files in `logs` folder
+ *      - `PHPLog`: php log
+ *      - `STDErr`: standard error output, usually written to web server error log
+ * - title: translatable title
+ * - active: 0 or 1
+ * - class_name: user defined class name which is derived from BxDolLogs.
+ * - class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ * 
+ * @section example Example of usage
+ *
+ * Log data to the logs object
+ *
+ * @code
+ *  if ($o = BxDolLogs::getObjectInstance('my_module_logs_object'))
+ *      $o->add($s);
+ * @endcode
+ *
+ * or
+ *
+ * @code
+ *  bx_log('my_module_logs_object', $s);
+ * @endcode
+ *
+ */
+class BxDolLogs extends BxDolFactory implements iBxDolFactoryObject
+{
+	protected $_oDb;
+	protected $_sObject;
+    protected $_aObject;
+    protected $_oLogsStorage;
+
+    /**
+     * Constructor
+     */
+    protected function __construct($aObject, $oLogsStorage)
+    {
+        parent::__construct();
+
+        $this->_aObject = $aObject;
+        $this->_sObject = $aObject['object'];
+
+        $this->_oLogsStorage = $oLogsStorage;
+        $this->_oDb = new BxDolLogsQuery($this->_aObject);
+    }
+
+   /**
+     * Get logs 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']['BxDolLogs!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolLogs!' . $sObject];
+
+        $aObject = BxDolLogsQuery::getLogsObject($sObject);
+        if($aObject && is_array($aObject) && !$aObject['active'])
+            return false;
+        if(!$aObject) {
+            $aObject = array(
+                'object' => $sObject,
+                'module' => 'system',
+                'logs_storage' => getParam('sys_logs_storage_default'),
+            );
+        }
+
+        $sClass = 'BxDolLogs';
+        if(!empty($aObject['class_name'])) {
+            $sClass = $aObject['class_name'];
+            if(!empty($aObject['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['class_file']);
+        }        
+
+        $sLogsStorage = (empty($aObject['logs_storage']) ? 'Auto' : $aObject['logs_storage']);
+        if ('Auto' == $sLogsStorage)
+            $sLogsStorage = getParam('sys_logs_storage_default');
+        $sClassLogsStorage = 'BxDolLogsStorage' . $sLogsStorage;
+        $oLogsStorage = $sClassLogsStorage::getInstance();
+
+        $o = new $sClass($aObject, $oLogsStorage);
+        return ($GLOBALS['bxDolClasses']['BxDolLogs!' . $sObject] = $o);
+    }
+
+    /**
+     * Get current logs object name
+     */
+    public function getObjectName()
+    {
+        return $this->_aObject['object'];
+    }
+
+    /**
+     * Log to the current logs storage object
+     * @param $mixed string or array to log
+     * @return true on success or false on error
+     */
+    public function add($mixed)
+    {
+        return $this->_oLogsStorage->add($this, $mixed);
+    }
+
+    /**
+     * Get logs from current logs storage
+     * @param $iLines number of lines from the tail to return
+     * @param $sFilter filter lines by keyword
+     * @return array of strings or false on error
+     */
+    public function get($iLines = 30, $sFilter = '')
+    {
+        if (!$this->_oLogsStorage->isGetAvail())
+            return false;
+        return $this->_oLogsStorage->get($this, $iLines, $sFilter);
+    }
+
+    /**
+     * Check if filtering supported in `get` method
+     */
+    public function isFilterAvail()
+    {
+        return $this->_oLogsStorage->isFilterAvail();
+    }
+
+    /**
+     * Check if `get` method available
+     */
+    public function isGetAvail()
+    {
+        return $this->_oLogsStorage->isGetAvail();
+    }
+}
+
+/** @} */

+ 33 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsQuery.php

@@ -0,0 +1,33 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLogsQuery extends BxDolDb
+{
+
+    public function __construct($aObject = array())
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getLogsObject($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_logs` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+}
+
+/** @} */

+ 137 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStorageFolder.php

@@ -0,0 +1,137 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLogsStorageFolder extends BxDolFactory implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolLogsStorageFolder();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Write to log 
+     * @param $oObject logs object 
+     * @param $mixed string or array to log
+     * @return true on success or false on error
+     */
+    public function add($oObject, $mixed)
+    {
+        if (!$mixed)
+            return true;
+
+        $sFile = BX_DIRECTORY_PATH_LOGS . $oObject->getObjectName() . '.log';
+
+        $s = $this->formatLogString($oObject, $mixed);
+
+        $bNewFile = !file_exists($sFile);
+        $bRet = file_put_contents($sFile, $s, FILE_APPEND) ? true : false;
+        if ($bNewFile && $bRet)
+            chmod($sFile, BX_DOL_FILE_RIGHTS);
+        return $bRet;
+    }
+
+    protected function formatLogString($oObject, $mixed, $bIncludeObjectName = false)
+    {
+        $s = date('M d H:i:s');
+
+        if ($bIncludeObjectName)
+            $s .= ' ' . $oObject->getObjectName();
+
+        $s .= ' [' . (int)bx_get_logged_profile_id() . '] ';
+
+        if (!empty($_SERVER['REQUEST_URI']))
+            $s .= $_SERVER['REQUEST_URI'] . (!empty($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '');
+        elseif (defined('BX_DOL_CRON_EXECUTE'))
+            $s .= 'CRON';
+        else
+            $s .= 'EMPTY';
+
+        $s .= ' ' . trim(is_array($mixed) ? print_r($mixed) : $mixed);
+
+        $s .= "\n";
+
+        return $s;
+    }
+
+    /**
+     * Get logs
+     * @param $oObject logs object
+     * @param $iLines number of lines from the tail
+     * @param $sFilter filter lines which include this phrase
+     * @return array of strings
+     */
+    public function get($oObject, $iLines, $sFilter = false)
+    {
+        $sFile = BX_DIRECTORY_PATH_LOGS . $oObject->getObjectName() . '.log';
+        $iSize = filesize($sFile);
+        if (!($fd = fopen($sFile, 'r+')))
+            return false;
+        $iPos = $iSize;
+        $n = 0;
+        while ($n < $iLines+1 && $iPos > 0) {
+            if (-1 == fseek($fd, $iPos))
+                break;
+            if (false === ($s = fread($fd, 1)))
+                break;
+            if ($s === "\n")
+                ++$n;
+            $iPos--;
+        }
+        $a = array();
+        for ($i = 0; $i < $iLines; $i++) {
+            if (false === ($s = fgets($fd)))
+                break;
+            if (!$sFilter || false !== stripos($s, $sFilter))
+                array_push($a, $s);
+        }
+        fclose($fd);
+        return $a;
+    }
+
+    /**
+     * Check if filtering supported in `get` method
+     */
+    public function isFilterAvail()
+    {
+        return true;
+    }
+
+    /**
+     * Check if `get` method available
+     */
+    public function isGetAvail()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 64 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStoragePHPLog.php

@@ -0,0 +1,64 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLogsStoragePHPLog extends BxDolLogsStorageFolder implements iBxDolSingleton
+{
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolLogsStoragePHPLog();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Write to log 
+     * @param $oObject logs object 
+     * @param $mixed string or array to log
+     * @return true on success or false on error
+     */
+    public function add($oObject, $mixed)
+    {
+        if (!$mixed)
+            return true;
+
+        $s = $this->formatLogString($oObject, $mixed, true);
+
+        return error_log($s, 0);
+    }
+
+    /**
+     * Not supported
+     */
+    public function get($oObject, $iLines, $sFilter = false)
+    {
+        return false;
+    }
+
+    /**
+     * Check if filtering supported in `get` method
+     */
+    public function isFilterAvail()
+    {
+        return false;
+    }
+
+    /**
+     * Check if `get` method available
+     */
+    public function isGetAvail()
+    {
+        return false;
+    }
+}
+
+/** @} */

+ 70 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolLogsStorageSTDErr.php

@@ -0,0 +1,70 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLogsStorageSTDErr extends BxDolLogsStorageFolder implements iBxDolSingleton
+{
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolLogsStorageSTDErr();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Write to log 
+     * @param $oObject logs object 
+     * @param $mixed string or array to log
+     * @return true on success or false on error
+     */
+    public function add($oObject, $mixed)
+    {
+        if (!$mixed)
+            return true;
+
+        $s = $this->formatLogString($oObject, $mixed, true);
+
+        if (!($fd = fopen('php://stderr', 'w')))
+            return false;
+        if (false === fwrite($fd, $s))
+            return false;
+        fclose($fd);
+
+        return true;
+    }
+
+    /**
+     * Not supported
+     */
+    public function get($oObject, $iLines, $sFilter = false)
+    {
+        return false;
+    }
+
+    /**
+     * Check if filtering supported in `get` method
+     */
+    public function isFilterAvail()
+    {
+        return false;
+    }
+
+    /**
+     * Check if `get` method available
+     */
+    public function isGetAvail()
+    {
+        return false;
+    }
+}
+
+/** @} */

+ 338 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMenu.php

@@ -0,0 +1,338 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Menus.
+ *
+ * Menu uses some set of items and the template to display it,
+ * so it is possible to have several menus which uses the same set of items but different templates.
+ *
+ * Menu is any set of some links or actions, for example menu can be links in site's footer or actions in profile view.
+ *
+ *
+ * @section menu_create Creating the Menu object:
+ *
+ * 1. Add record to 'sys_objects_menu' table:
+ *
+ * - object: name of the menu object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_groups_actions - actions menu in group view.
+ * - title: name of the menu, displayed in the studio menu builder.
+ * - set_name: name of items' set.
+ * - module: the module this menu belongs to.
+ * - template_id: the template to use for menu displaying, this is id from 'sys_menu_templates' table.
+ * - deletable: it determines if menu can be deleted from the studio menu builder.
+ * - active: it is possible to disable particular menu, then it will not be displayed.
+ * - override_class_name: user defined class name which is derived from BxTemplMenu.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Menu templates are stored in 'sys_menu_templates' table:
+ * - id: template id.
+ * - template: template file.
+ * - title: template title to display in the studio menu builder.
+ * All menu templates iterate through 'bx_repeat:menu_items' and use the following template variables for each menu item: __link__, __target__, __onclick__, __title__, __class_add__.
+ *
+ *
+ * 2. Add menu an empty menu set to 'sys_menu_sets' table (if you want to use new set of items for created menu):
+ * - set_name: the set name.
+ * - module: the module this set belongs to.
+ * - title: name of the set, displayed in studio menu builder.
+ * - deletable: it determines if the set can be deleted from menu builder.
+ *
+ *
+ * 3. Add menu items to the set by adding records to 'sys_menu_items' table:
+ * - set_name: the set name this item belogs to.
+ * - module: the module this item belongs to.
+ * - name: name of the item (not displayed to the end user), unique in the particular set.
+ * - title: menu item title to display to the end user, please note that some templates can still display menu as icons without text titles.
+ * - link: menu item URL.
+ * - onclick: menu item onclick event.
+ * - target: menu item target.
+ * - icon: menu item icon, please note that some templates can still display menu as text without icons.
+ * - addon: display additional data near menu item, only for supported menu templates, this is serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - markers: service method to provide additional replacement markers, this is serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - active: it is possible to disable particular menu item, then it will not be displayed.
+ * - order: menu item order in the particular set.
+ *
+ *
+ * 4. Display Menu.
+ * Use the following sample code to display menu:
+ * @code
+ *     $oMenu = BxTemplMenu::getObjectInstance('sample_menu'); // 'sample_menu' is 'object' field from 'sys_objects_menu' table.
+ *     if ($oMenu)
+ *         echo $oMenu->getCode; // display menu
+ * @endcode
+ *
+ * But in most cases you don't need to use above code to display menu,
+ * menu objects are integrated into pages - there is special 'menu' page block type for it.
+ *
+ */
+class BxDolMenu extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected static $SEL_MODULE = '';
+    protected static $SEL_NAME = '';
+
+    protected $_bDynamicMode;
+    protected $_bAddNoFollow;
+
+    protected $_sSelModule;
+    protected $_sSelName;
+
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_oPermalinks;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of menu options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_bDynamicMode = false;
+        $this->_bAddNoFollow = getParam('sys_add_nofollow') == 'on';
+
+        $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(),
+                	'content_id' => $oProfile->getContentId()
+                ));
+            }
+        }
+    }
+
+    /**
+     * 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) || (int)$aObject['active'] == 0)
+            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;
+
+                $mixedMenuSet = BxDolService::call($aModule['name'], 'get_menu_set_name_for_menu_trigger', array($sMenuTriggerName));
+                if(empty($mixedMenuSet))
+                    continue;
+
+                if(is_string($mixedMenuSet))
+                    $mixedMenuSet = array($mixedMenuSet);
+
+                foreach($mixedMenuSet as $sMenuSet) {
+                    if(empty($sMenuSet))
+                        continue;
+
+                    $aMenuItem['set_name'] = $sMenuSet;
+                    BxDolMenuQuery::addMenuItemToSet($aMenuItem);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if the menu is visible. The menu is visible if at least one menu item is visible.
+     * @return boolean
+     */
+    public function isVisible()
+    {
+        if((int)$this->_aObject['active'] == 0)
+            return false;
+
+    	if(!isset($this->_aObject['menu_items']))
+			$this->_aObject['menu_items'] = $this->_oQuery->getMenuItems();
+
+    	$bVisible = false;
+    	foreach ($this->_aObject['menu_items'] as $a) {
+    		if((isset($a['active']) && !$a['active']) || (isset($a['visible_for_levels']) && !$this->_isVisible($a)))
+				continue;
+			
+			$bVisible = true;
+			break;
+    	}
+
+    	return $bVisible;
+    }
+
+    public function getTemplateId()
+    {
+        return $this->_aObject['template_id'];
+    }
+
+    public function setTemplateById ($iTemplateId)
+    {
+        $aTemplate = $this->_oQuery->getMenuTemplateById($iTemplateId);
+        if(empty($aTemplate) || !is_array($aTemplate))
+            return;
+
+        $this->_aObject['template'] = $aTemplate['template'];
+    }
+    
+    /**
+     * Set selected menu item for current menu object only.
+     * @param $sModule menu item module to set as selected
+     * @param $sName menu item name to set as selected
+     */
+    public function setSelected ($sModule, $sName)
+    {
+        $this->_sSelModule = $sModule;
+        $this->_sSelName = $sName;
+    }
+
+    public function setDynamicMode ($bDynamicMode)
+    {
+        $this->_bDynamicMode = $bDynamicMode;
+    }
+
+    /**
+     * Get an arrey of replacable markers.
+     * @return an array with markers
+     */
+    public function getMarkers()
+    {
+        return $this->_aMarkers;
+    }
+    
+    /**
+     * 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)
+    {
+        if (!BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']))
+            return false;
+
+        if (!empty($a['visibility_custom']))
+            return BxDolService::callSerialized($a['visibility_custom'], $this->_aMarkers);
+        
+        return true;
+    }
+
+    /**
+     * 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;
+    }
+
+}
+
+/** @} */

+ 127 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMenuQuery.php

@@ -0,0 +1,127 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_MENU_LAST_ITEM_ORDER', 9999);
+
+/**
+ * Database queries for menus.
+ * @see BxDolMenu
+ */
+class BxDolMenuQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getMenuObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `o`.*, `t`.`template` FROM `sys_objects_menu` AS `o` INNER JOIN `sys_menu_templates` AS `t` ON (`t`.`id` = `o`.`template_id`) WHERE `o`.`object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getMenuTriggers($sTriggerName)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_menu_items` WHERE `set_name` = ? ORDER BY `order` DESC", $sTriggerName);
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function addMenuItemToSet($aMenuItem)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        if (empty($aMenuItem['set_name']))
+            return false;
+
+        // check if menu item already exists, 
+        // so the menu item position will not reset when it's unnecessary
+        $sQuery = $oDb->prepare("SELECT `id` FROM `sys_menu_items` WHERE `set_name` = ? AND `name` = ?", $aMenuItem['set_name'], $aMenuItem['name']);
+        if ($oDb->getOne($sQuery))
+            return true;
+      
+        // get order
+        if (empty($aMenuItem['order'])) {
+            $sQuery = $oDb->prepare("SELECT `order` FROM `sys_menu_items` WHERE `set_name` = ? AND `active` = 1 AND `order` != ? ORDER BY `order` DESC LIMIT 1", $aMenuItem['set_name'], BX_MENU_LAST_ITEM_ORDER);
+            $iProfileMenuOrder = (int)$oDb->getOne($sQuery);
+            $aMenuItem['order'] = $iProfileMenuOrder + 1;
+        }
+
+        // add new item
+        unset($aMenuItem['id']);
+        return $oDb->query("INSERT INTO `sys_menu_items` SET " . $oDb->arrayToSQL($aMenuItem));
+    }
+
+    public function getMenuItems()
+    {
+        return $this->getMenuItemsFromSet($this->_aObject['set_name']);
+    }
+
+    public function getMenuItemsFromSet($sSetName)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_menu_items` WHERE `set_name` = ? ORDER BY `order` ASC", $sSetName);
+        return $this->getAllWithKey($sQuery, 'name');
+    }
+
+    public function getMenuItemsBy($aParams = array())
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $aBindings = array();
+
+    	$sSelectClause = '*';
+    	$sWhereClause = $sGroupClause = $sOrderClause = '';
+    	$sLimitClause = isset($aParams['start']) && !empty($aParams['per_page']) ? " LIMIT " . $aParams['start'] . ", " . $aParams['per_page'] : "";
+
+    	if(!empty($aParams['type']))
+            switch($aParams['type']) {
+                case 'set_name':
+                    $aBindings['set_name'] = $aParams['set_name'];
+
+                    $sWhereClause = 'AND `set_name` = :set_name';
+                    $sOrderClause = '`order` ASC';
+                    break;
+                    
+                case 'set_name_duplicates':
+                    $aMethod['name'] = 'getColumn';
+                    $aBindings['set_name'] = $aParams['set_name'];
+
+                    $sSelectClause = '`name`';
+                    $sWhereClause = 'AND `set_name` = :set_name';
+                    $sGroupClause = '`name` HAVING COUNT(`id`) > 1';
+                    break;
+            }
+
+        if(!empty($sGroupClause))
+            $sGroupClause = " GROUP BY " . $sGroupClause;
+
+        if(!empty($sOrderClause))
+            $sOrderClause = " ORDER BY " . $sOrderClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `sys_menu_items` WHERE 1 " . $sWhereClause . $sGroupClause . $sOrderClause . $sLimitClause;
+        $aMethod['params'][] = $aBindings;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+    
+    public function getMenuTemplateById($iId, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_menu_templates` WHERE `id`=? LIMIT 1", $iId);
+        return $bFromCache ? $this->fromMemory('sys_menu_templates_' . $iId, 'getRow', $sSql) : $this->getRow($sSql);
+    }
+}
+
+/** @} */

+ 1009 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMetatags.php

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

+ 154 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolMetatagsQuery.php

@@ -0,0 +1,154 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for metatags objects.
+ * @see BxDolMetatag
+ */
+class BxDolMetatagsQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getMetatagsObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_metatags` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+    
+    public function mentionsAdd($mixedContentId, $aMentions, $bDeletePrevious = true)
+    {
+        return $this->metaAdd($mixedContentId, $aMentions, $bDeletePrevious, 'mentionsDelete', $this->_aObject['table_mentions'], 'profile_id');
+    }
+
+    public function mentionsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_mentions'], $mixedContentId);
+    }
+
+    public function mentionsGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT `profile_id` FROM `{$this->_aObject['table_mentions']}` WHERE `object_id` = ? ORDER BY `profile_id` DESC LIMIT ?", $mixedContentId, (int)getParam('sys_metatags_mentions_max'));
+        return $this->getColumn($sQuery);
+    }
+    
+    public function keywordsAdd($mixedContentId, $aKeywords, $bDeletePreviousKeywords = true)
+    {
+        return $this->metaAdd($mixedContentId, $aKeywords, $bDeletePreviousKeywords, 'keywordsDelete', $this->_aObject['table_keywords'], 'keyword');
+    }
+
+    public function keywordsDelete($mixedContentId)
+    {
+        $mixedResult = $this->metaDelete($this->_aObject['table_keywords'], $mixedContentId);
+        if ($mixedResult != false)
+            bx_alert('meta_keyword', 'deleted', $mixedContentId, bx_get_logged_profile_id(), array('object' => $this->_aObject['object']));
+        return $mixedResult;
+    }
+
+    public function keywordsGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT `keyword` FROM `{$this->_aObject['table_keywords']}` WHERE `object_id` = ? ORDER BY CHAR_LENGTH(`keyword`) DESC LIMIT ?", $mixedContentId, (int)getParam('sys_metatags_hashtags_max'));
+        return $this->getColumn($sQuery);
+    }
+
+    public function keywordsGetSQLParts($sContentTable, $sContentField, $mixedKeyword)
+    {
+        if(!is_array($mixedKeyword))
+            $mixedKeyword = array($mixedKeyword);
+
+        return array(
+            'where' => !empty($mixedKeyword) ? ' AND `tt`.`keyword` IN (' . $this->implode_escape($mixedKeyword) . ')' : '',
+            'join' => 'INNER JOIN `' . $this->_aObject['table_keywords'] . '` AS `tt` ON `' . $sContentTable . '`.`' . $sContentField . '`=`tt`.`object_id`'
+        );
+    }
+
+    public function keywordsPopularList($iLimit)
+    {
+        $sQuery = $this->prepare("SELECT `keyword`, COUNT(*) as `count` FROM `{$this->_aObject['table_keywords']}` GROUP BY `keyword` ORDER BY `count` DESC LIMIT ?", $iLimit);
+        return $this->getPairs($sQuery, 'keyword', 'count');
+    }    
+
+
+
+    public function locationsAdd($mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber)
+    {
+        $this->locationsDelete($mixedContentId);
+        if (!$fLat && !$fLng) // locations without latitude and longitude aren't allowed
+            return true;
+
+        $sQuery = $this->prepare("INSERT INTO `{$this->_aObject['table_locations']}` SET `object_id` = ?, `lat` = ?, `lng` = ?, `country` = ?, `state` = ?, `city` = ?, `zip` = ?, `street` = ?, `street_number` = ?", $mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber);
+        return $this->query($sQuery);
+    }
+    
+    public function locationsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_locations'], $mixedContentId);
+    }
+
+    public function locationGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `{$this->_aObject['table_locations']}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->getRow($sQuery);
+    }
+
+    public function locationsGetSQLParts($sContentTable, $sContentField, $sCountry = false, $sState = false, $sCity = false, $sZip = false, $aBounds = array())
+    {
+        $aFields = array('country' => 'sCountry', 'state' => 'sState', 'city' => 'sCity', 'zip' => 'sZip', 'bounds' => 'aBounds');
+
+        $aWhere = array();
+        $sWhereBounds = '';
+        foreach ($aFields as $sIndex => $sVar) {
+            if (!$$sVar)
+                continue;
+
+            if ('bounds' == $sIndex) {
+                $sWhereBounds = $this->prepareAsString(" AND `tl`.`lat` != 0 AND `tl`.`lng` != 0 AND `tl`.`lat` > ? AND `tl`.`lat` < ? AND `tl`.`lng` > ? AND `tl`.`lng` < ?", ${$sVar}['min_lat'], ${$sVar}['max_lat'], ${$sVar}['min_lng'], ${$sVar}['max_lng']);
+            } 
+            else {
+                $aWhere['tl`.`' . $sIndex] = $$sVar;
+            }
+        }
+
+        return array(
+            'where' => $sWhereBounds . (!empty($aWhere) ? ' AND ' . $this->arrayToSQL($aWhere, ' AND ') : ''),
+            'join' => 'INNER JOIN `' . $this->_aObject['table_locations'] . '` AS `tl` ON `' . $sContentTable . '`.`' . $sContentField . '`=`tl`.`object_id`'
+        );
+    }
+
+    protected function metaDelete($sTable, $mixedContentId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$sTable}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->query($sQuery);
+    }
+
+    protected function metaAdd($mixedContentId, $aMetas, $bDeletePrevious, $sFuncDelete, $sTable, $sField)
+    {
+        if ($bDeletePrevious)
+            $this->$sFuncDelete($mixedContentId);
+
+        $i = 0;
+        foreach ($aMetas as $sMeta) {
+            $sQuery = $this->prepare("INSERT INTO `{$sTable}` SET `object_id` = ?, `{$sField}` = ?", $mixedContentId, $sMeta);
+            $i += ($this->query($sQuery) ? 1 : 0);
+        }
+        return $i;
+    }
+}
+
+/** @} */

+ 194 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolModule.php

@@ -0,0 +1,194 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Base class for Module classes in modules engine.
+ *
+ * The object of the class contains major objects of the whole module. They are:
+ * a. An object of config class
+ * @see BxDolModuleConfig
+ *
+ * b. An object of database class.
+ * @see BxDolModuleQuery
+ *
+ * c. An object of template class.
+ * @see BxDolTemplate
+ *
+ *
+ * Example of usage:
+ * refer to any BoonEx module
+ *
+ *
+ * Static Methods:
+ *
+ * Get an instance of a module's class.
+ * @see BxDolModule::getInstance($sClassName)
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolModule extends BxDol
+{
+    public $_aModule;
+    public $_oDb;
+    public $_oTemplate;
+    public $_oConfig;
+
+    /**
+     * constructor
+     */
+    public function __construct($aModule)
+    {
+        parent::__construct();
+
+        $this->_aModule = $aModule;
+
+        $sClassPrefix = $aModule['class_prefix'];
+        $sClassPath = BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'classes/';
+
+        $sClassName = $sClassPrefix . 'Config';
+        require_once($sClassPath . $sClassName . '.php');
+        $this->_oConfig = new $sClassName($aModule);
+
+        $sClassName = $sClassPrefix . 'Db';
+        require_once($sClassPath . $sClassName . '.php');
+        $this->_oDb = new $sClassName($this->_oConfig);
+
+        $sClassName = $sClassPrefix . 'Template';
+        require_once($sClassPath . $sClassName . '.php');
+        $oTemplate = new $sClassName($this->_oConfig, $this->_oDb); 
+        $oTemplate->init();
+        $this->_oTemplate = new BxDolModuleProxy('module_template', $oTemplate);
+    }
+
+    /**
+     * 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();
+    }
+
+    public function serviceIsSafeService($s)
+    {
+        $sService = bx_gen_method_name($s);
+        $aServices = $this->serviceGetSafeServices();
+        return isset($aServices[$sService]);
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array();
+    }
+
+    public function serviceIsPublicService($s)
+    {
+        $sService = bx_gen_method_name($s);
+        $aServices = $this->serviceGetPublicServices();
+        return isset($aServices[$sService]);
+    }
+
+    public function serviceGetPublicServices()
+    {
+        return array();
+    }
+}
+
+/** @} */

+ 214 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolModuleQuery.php

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

+ 345 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPage.php

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

+ 115 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPageQuery.php

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

+ 421 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPayments.php

@@ -0,0 +1,421 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolPayments extends BxDolFactory implements iBxDolSingleton
+{
+	protected $_oDb;
+
+	protected $_aObjects;
+	protected $_sActive;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolPaymentsQuery();
+
+        $this->_aObjects = $this->_oDb->getObjects();
+        $this->_sActive = getParam('sys_default_payment');
+    }
+
+	static public function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses']['BxDolPayments']))
+        	$GLOBALS['bxDolClasses']['BxDolPayments'] = new BxDolPayments();
+
+		return $GLOBALS['bxDolClasses']['BxDolPayments'];
+    }
+
+	public function setActive($sActive)
+    {
+		$this->_sActive = $sActive;
+    }
+
+    public function getActive()
+    {
+    	return $this->_sActive;
+    }
+
+    public function isActive()
+    {
+    	if(empty($this->_sActive))
+            return false;
+
+    	if(!BxDolModuleQuery::getInstance()->isModuleByName($this->_sActive))
+            return false;
+
+    	return true;
+    }
+
+    public function isCreditsOnly()
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_credits_only'))
+            return false;
+
+        return bx_srv($this->_sActive, 'is_credits_only');
+    }
+
+    public function getDetailsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_details_url', 'Details'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_details_url', array(), 'Details');
+    }
+
+    public function isAcceptingPayments($iVendorId, $sPaymentType = '')
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_accepting_payments'))
+    		return false;
+
+    	$aSrvParams = array($iVendorId, $sPaymentType);
+        return bx_srv($this->_sActive, 'is_accepting_payments', $aSrvParams);
+    }
+
+    /**
+     * @deprecated since version 11.0.0
+     * 
+     * @see BxDolPayments::isProviderOptions
+     */
+    public function isPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	return $this->isProviderOptions($iVendorId, $sVendorProvider);
+    }
+
+    /**
+     * @deprecated since version 11.0.0
+     * 
+     * @see BxDolPayments::getProviderOptions
+     */
+    public function getPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	return $this->getProviderOptions($iVendorId, $sVendorProvider, $sPaymentType);
+    }
+
+    public function isProviderOptions($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_provider_options'))
+            return false;
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return bx_srv($this->_sActive, 'is_provider_options', $aSrvParams);
+    }
+
+    public function getProvider($sProvider, $mixedVendorId = BX_PAYMENT_EMPTY_ID)
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_provider'))
+            return false;
+
+        $aSrvParams = array($sProvider, $mixedVendorId);
+        return bx_srv($this->_sActive, 'get_provider', $aSrvParams);
+    }
+
+    public function getProviderOptions($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_provider_options'))
+            return false;
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return bx_srv($this->_sActive, 'get_provider_options', $aSrvParams);
+    }
+
+    public function getModulesWithPayments()
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_modules_with_payments'))
+            return false;
+
+        return bx_srv($this->_sActive, 'get_modules_with_payments', array());
+    }
+
+    public function getProductsNames($iVendor = 0, $iLimit = 1000)
+    {
+        $aRet = array();
+        $aModules = $this->getModulesWithPayments();
+        foreach ($aModules as $sModule) {
+            if(!BxDolRequest::serviceExists($sModule, 'get_products_names'))
+                continue;
+            $aKeys = bx_srv($sModule, 'get_products_names', array($iVendor, $iLimit));
+            if (!$aKeys)
+                continue;
+            $aValues = array_fill(0, count($aKeys), $sModule);
+            $aRet = array_merge($aRet, array_combine($aKeys, $aValues));
+        }
+
+        return $aRet;
+    }
+
+    public function getPayments()
+    {
+        $aPayments = array(
+			'' => _t('_Select_one')
+        );
+		foreach($this->_aObjects as $aObject) {
+			if(empty($aObject) || !is_array($aObject))
+				continue;
+
+			$aPayments[$aObject['object']] = _t($aObject['title']);
+		}
+
+        return $aPayments;
+    }
+
+    public function updateDependentModules($sModule = 'all', $bInstall = true)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'update_dependent_modules'))
+            return;
+
+        bx_srv($this->_sActive, 'update_dependent_modules', array($sModule, $bInstall));
+    }
+
+    public function getProvidersCart($iVendorId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_providers_cart'))
+    		return array();
+
+    	$aSrvParams = array($iVendorId);
+        return bx_srv($this->_sActive, 'get_providers_cart', $aSrvParams);
+    }
+
+    public function getOption($sOption)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_option'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_option', array($sOption));
+    }
+
+    public function getOrdersUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_url', 'Orders'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_orders_url', array(), 'Orders');
+    }
+
+    public function getOrdersCount($sType)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_count', 'Orders'))
+            return array();
+
+        $aSrvParams = array($sType);
+        return bx_srv($this->_sActive, 'get_orders_count', $aSrvParams, 'Orders');
+    }
+
+    public function getOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_info', 'Orders'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getPendingOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_pending_orders_info', 'Orders'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_pending_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getSubscriptionOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscription_orders_info', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_subscription_orders_info', $aSrvParams, 'Subscriptions');
+    }
+
+    public function getSubscriptionsInfo($aConditions, $bCheckInProvider = false)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscriptions_info', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($aConditions, $bCheckInProvider);
+        return bx_srv($this->_sActive, 'get_subscriptions_info', $aSrvParams, 'Subscriptions');
+    }
+
+    public function getCartUrl($iVendor = 0)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_url', 'Cart'))
+    		return '';
+
+    	return bx_srv($this->_sActive, 'get_cart_url', array($iVendor), 'Cart');
+    }
+
+    public function getCartItemsCount()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_items_count', 'Cart'))
+            return 0;
+
+        $aSrvParams = array();
+        return bx_srv($this->_sActive, 'get_cart_items_count', $aSrvParams, 'Cart');
+    }
+    
+    public function getCartItems($iVendorId, $iModuleId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_items', 'Cart'))
+            return 0;
+
+        $aSrvParams = array($iVendorId, $iModuleId);
+        return bx_srv($this->_sActive, 'get_cart_items', $aSrvParams, 'Cart');
+    }
+
+    public function getCartItemDescriptor($iVendorId, $iModuleId, $iItemId, $iItemCount)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_item_descriptor', 'Cart'))
+            return '';
+
+    	$aSrvParams = array($iVendorId, $iModuleId, $iItemId, $iItemCount);
+        return bx_srv($this->_sActive, 'get_cart_item_descriptor', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns cart JavaScript code which should be included in the page to make "Add To Cart" and "Subscribe" buttons work properly.
+     */
+    public function getCartJs($sType = '', $iVendorId = 0)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_js', 'Cart'))
+            return '';
+
+        $aSrvParams = array($sType, $iVendorId);
+        return bx_srv($this->_sActive, 'get_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Adds an item to cart in background mode.
+     */
+	public function addToCart($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'add_to_cart', 'Cart'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $aCustom);
+        return bx_srv($this->_sActive, 'add_to_cart', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns "Add To Cart" JavaScript code to use in onclick attribute.
+     */
+    public function getAddToCartJs($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false, $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_js', 'Cart'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_add_to_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns the whole "Add To Cart" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getAddToCartLink($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false, $aCustom = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_link', 'Cart'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_add_to_cart_link', $aSrvParams, 'Cart');
+    }
+
+    public function getSubscriptionsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscriptions_url', 'Subscriptions'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_subscriptions_url', array(), 'Subscriptions');
+    }
+
+    public function getSubscribeUrl($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_url', 'Subscriptions'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_subscribe_url', array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount), 'Subscriptions');
+    }
+
+    /**
+     * Returns "Subscribe" JavaScript code to use in onclick attribute.
+     */
+    public function getSubscribeJs($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '', $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_js', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_js', $aSrvParams, 'Subscriptions');
+    }
+
+    /**
+     * Returns "Subscribe" JavaScript code to use in onclick attribute.
+     */
+    public function getSubscribeJsWithAddons($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sItemAddons = '', $sRedirect = '', $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_js_with_addons', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sItemAddons, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_js_with_addons', $aSrvParams, 'Subscriptions');
+    }
+
+    /**
+     * Returns the whole "Subscribe" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getSubscribeLink($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '', $aCustom = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_link', 'Subscriptions'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_link', $aSrvParams, 'Subscriptions');
+    }
+
+    public function sendSubscriptionExpirationLetters($iPendingId, $sOrderId)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'send_subscription_expiration_letters', 'Subscriptions'))
+            return '';
+
+        $aSrvParams = array($iPendingId, $sOrderId);
+        return bx_srv($this->_sActive, 'send_subscription_expiration_letters', $aSrvParams, 'Subscriptions');
+    }
+
+    public function initializeCheckout($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'initialize_checkout'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'initialize_checkout', $aSrvParams);
+    }
+
+    /**
+     * Isn't used anywhere for now. Is needed when license 'prolong' is requested from license keeping module like Market and ACL.  
+     */
+    public function prolongSubscription($sOrderId)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'prolong_subscription'))
+            return '';
+
+        $aSrvParams = array($sOrderId);
+        return bx_srv($this->_sActive, 'prolong_subscription', $aSrvParams);
+    }
+
+    public function generateLicense()
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'generate_license'))
+            return '';
+
+        $aSrvParams = array();
+        return bx_srv($this->_sActive, 'generate_license', $aSrvParams, 'Module', true);
+    }
+}
+
+/** @} */

+ 739 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPrivacy.php

@@ -0,0 +1,739 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_PG_HIDDEN', '1');
+define('BX_DOL_PG_MEONLY', '2');
+define('BX_DOL_PG_ALL', '3');
+define('BX_DOL_PG_MEMBERS', '4');
+define('BX_DOL_PG_FRIENDS', '5');
+define('BX_DOL_PG_FRIENDS_SELECTED', '6');
+define('BX_DOL_PG_RELATIONS', '7');
+define('BX_DOL_PG_RELATIONS_SELECTED', '8');
+define('BX_DOL_PG_CUSTOM', '9');
+
+define('BX_DOL_PG_DEFAULT', BX_DOL_PG_ALL);
+
+/**
+ * Privacy settings for any content.
+ *
+ * Integration of the content with privacy engine allows site member
+ * to organize the access to his content.
+ *
+ * In addition to regular privacy groups (Public, Friends), spaces are supported. 
+ * When some space (usually some another profile) is specified as privacy, 
+ * then another profile visibility is used to check the privacy.
+ *
+ * Related classes:
+ *  BxDolPrivacyQuery - database queries.
+ *
+ * Example of usage:
+ * 1. Register your privacy actions in `sys_privacy_actions` database table.
+ * 2. Add one privacy field(with INT type) in the table with your items for each action.
+ *    For example, for action 'comment', the field name should be 'allow_comment_to'.
+ * 3. Add group choosers for necessary actions in the form, which is used to add new items.
+ * @code
+ *    $oPrivacy = new BxDolPrivacy();
+ *    $oPrivacy->getGroupChooser($iItemOwnerId, $sModuleUri, $sModuleAction);
+ * @endcode
+ *
+ * 4. Check privacy when any user tries to view an item.
+ * @code
+ *    $oPrivacy = new BxDolPrivacy($sTable, $sFieldId, $sFieldOwnerId);
+ *    if($oPrivacy->check($sAction, $iObjectId, $iViewerId)) {
+ *     //show necessary content
+ *    }
+ * @endcode
+ *
+ *    @see an example of integration in BoonEx modules, for example: Posts
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolPrivacy extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_oDb;
+    protected $_sObject;
+    protected $_aObject;
+
+    protected $_aGroupsSettings;
+    protected $_aGroupsExclude;
+    
+    protected $_sFormUsers;
+    protected $_sFormDisplayUsersSelect;
+
+    /**
+     * Constructor
+     * @param $aObject array of grid options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_aObject = $aObject;
+        $this->_sObject = $aObject['object'];
+
+        $this->_oDb = new BxDolPrivacyQuery();
+        $this->_oDb->init($this->_aObject);
+
+        $this->_aGroupsSettings = array(
+            BX_DOL_PG_FRIENDS_SELECTED => array('connection' => 'sys_profiles_friends'),
+            BX_DOL_PG_RELATIONS_SELECTED => array('connection' => 'sys_profiles_relations')
+        );
+
+        $this->_aGroupsExclude = array();
+
+        $this->_sFormUsers = 'sys_privacy_group_custom';
+        $this->_sFormDisplayUsersSelect = 'sys_privacy_group_custom_manage';
+    }
+
+    /**
+     * Get privacy object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolPrivacy!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolPrivacy!' . $sObject];
+
+        $aObject = BxDolPrivacyQuery::getPrivacyObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplPrivacy';
+        if(!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if(!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject);
+        return ($GLOBALS['bxDolClasses']['BxDolPrivacy!' . $sObject] = $o);
+    }
+
+    /**
+     * Get Select element with available groups.
+     *
+     * @param  string  $sObject  privacy object name.
+     * @param  integer $iOwnerId object's owner ID.
+     * @param  array   $aParams  an array of custom selector's params (dynamic_groups - an array of arrays('key' => group_id, 'value' => group_title), title - the title to be used for generated field).
+     * @return an      array with Select element description.
+     */
+    public static function getGroupChooser($sObject, $iOwnerId = 0, $aParams = array())
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return array();
+
+        $sModule = $oPrivacy->_aObject['module'];
+        $sAction = $oPrivacy->_aObject['action'];
+
+        if($iOwnerId == 0)
+            $iOwnerId = bx_get_logged_profile_id();
+
+        $sValue = $oPrivacy->_oDb->getDefaultGroupByUser($sModule, $sAction, $iOwnerId);
+        if(empty($sValue))
+            $sValue = $oPrivacy->_oDb->getDefaultGroup($sModule, $sAction);
+
+        $aValues = $oPrivacy->getGroups();
+
+        $aValues = $oPrivacy->addDynamicGroups($aValues, $iOwnerId, $aParams);
+
+        $aValues = $oPrivacy->addAclGroups($aValues, $iOwnerId, $aParams);
+
+        $aValues = $oPrivacy->addSpaces($aValues, $iOwnerId, $aParams);
+
+        $sName = $oPrivacy->convertActionToField($sAction);
+
+        $sTitle = isset($aParams['title']) && !empty($aParams['title']) ? $aParams['title'] : '';
+        if(empty($sTitle)) {
+            $sTitle = $oPrivacy->_oDb->getTitle($sModule, $sAction);
+            $sTitle = _t(!empty($sTitle) ? $sTitle : '_' . $sName);
+        }
+
+        $bDynamicMode = (isset($aParams['dynamic_mode']) && (bool)$aParams['dynamic_mode'] === true) || bx_is_dynamic_request();
+        return array(
+            'type' => 'select',
+            'name' => $sName,
+            'caption' => $sTitle,
+            'value' => $sValue,
+            'values' => $aValues,
+            'attrs' => array(
+                'class' => 'sys-privacy-group',
+                'onchange' => 'javascript: ' . $oPrivacy->getJsObjectName() . '.selectGroup(this);'
+            ),
+            'checker' => array(
+                'func' => 'avail',
+                'error' => _t('_sys_ps_ferr_incorrect_select')
+            ),
+            'db' => array(
+                'pass' => 'Int'
+            ),
+            'content' => $oPrivacy->addCssJs($bDynamicMode)
+        );
+    }
+
+    public static function initGroupChooser($sObject, $iOwnerId = 0, $aParams = array())
+    {
+        $sResult = '';
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return $sResult;
+
+        $iOwnerId = !empty($iOwnerId) ? (int)$iOwnerId : bx_get_logged_profile_id();
+        $iContentId = !empty($aParams['content_id']) ? (int)$aParams['content_id'] : 0;
+        $iGroupId = !empty($aParams['group_id']) ? (int)$aParams['group_id'] : 0;
+        $bDynamicMode = (isset($aParams['dynamic_mode']) && (bool)$aParams['dynamic_mode'] === true) || bx_is_dynamic_request();
+
+        $sJsCodeAdd = '';
+        if($oPrivacy->isGroupsCustom())
+            $sJsCodeAdd = $oPrivacy->getLoadGroupCustom($iOwnerId, $iContentId, $iGroupId, isset($aParams['html_ids']) ? $aParams['html_ids'] : array());
+
+        return $oPrivacy->getJsScript($sJsCodeAdd, $bDynamicMode);
+    }
+    
+    public static function getIcon($iVisibility)
+    {
+        $aIcons =array(
+           BX_DOL_PG_MEONLY => 'lock',
+           BX_DOL_PG_ALL => 'globe',
+           BX_DOL_PG_FRIENDS => 'user-friends',
+           BX_DOL_PG_FRIENDS => 'user-friends'
+       );
+       return isset($aIcons[$iVisibility]) ? $aIcons[$iVisibility] : 'eye';
+    }
+    
+            
+    public function actionLoadGroupCustom()
+    {
+        $iProfileId = (int)bx_get('profile_id');
+        $iContentId = (int)bx_get('content_id');
+        $iGroupId = (int)bx_get('group_id');
+
+        $oForm = BxDolForm::getObjectInstance($this->_sFormUsers, $this->_sFormDisplayUsersSelect);
+        return echoJson(array('eval' => $this->getJsObjectName() . '.onSelectGroup(oData);', 'content' => $oForm->getElementGroupCustom(array(
+            'profile_id' => $iProfileId, 
+            'content_id' => $iContentId, 
+            'object' => $this->_sObject, 
+            'group_id' => $iGroupId
+        ))));
+    }
+
+    public function actionSelectGroup()
+    {
+        $aValues = array(
+            'profile_id' => (int)bx_get('profile_id'),
+            'content_id' => (int)bx_get('content_id'),
+            'object' => $this->_sObject,
+            'group_id' => (int)bx_get('group_id')
+        );
+
+        $aParams = array(
+            'popup_only' => (bool)bx_get('popup_only')
+        );
+
+        if(!isset($this->_aGroupsSettings[$aValues['group_id']]))
+            return echoJson(array());
+
+        if($aValues['profile_id'] != bx_get_logged_profile_id())
+            return echoJson(array('msg' => _t('_sys_ps_ferr_incorrect_gc_owner')));
+
+        if(($mixedResult = $this->isSelectGroupCustom($aValues)) !== true)
+            return echoJson(array('msg' => $mixedResult));
+
+        return echoJson($this->getSelectGroup($aValues, $aParams));
+    }
+
+    public function actionUsersList()
+    {
+        $iGroup = (int)bx_get('group');
+        if(!isset($this->_aGroupsSettings[$iGroup]))
+            return echoJson(array());
+
+        $oConnection = BxDolConnection::getObjectInstance($this->_aGroupsSettings[$iGroup]['connection']);
+        if(!$oConnection)
+            return echoJson(array());
+
+        $iProfileId = bx_get_logged_profile_id();
+        $aConnectedIds = $oConnection->getConnectedContent($iProfileId, true);
+        if(empty($aConnectedIds) || !is_array($aConnectedIds))
+            return echoJson(array());
+
+        $sTerm = bx_get('term');
+        $aProfiles = BxDolService::call('system', 'profiles_search', array($sTerm), 'TemplServiceProfiles');
+        if(empty($aProfiles))
+            return echoJson(array());
+        
+        $aResult = array();
+        foreach($aProfiles as $aProfile)
+            if(in_array($aProfile['value'], $aConnectedIds))
+                $aResult[] = $aProfile;
+
+        echoJson($aResult);
+    }
+
+    public function getGroupSettings($iGroup)
+    {
+        if(empty($this->_aGroupsSettings[$iGroup]) || !is_array($this->_aGroupsSettings[$iGroup]))
+            return false;
+
+        return $this->_aGroupsSettings[$iGroup];
+    }
+
+    public function isGroupsCustom()
+    {
+        $aGroups = $this->_oDb->getGroupsBy(array('type' => 'active_list'));
+
+        return in_array(BX_DOL_PG_FRIENDS_SELECTED, $aGroups) || in_array(BX_DOL_PG_RELATIONS_SELECTED, $aGroups);                
+    }
+        
+    public function getGroupsBy($aParams)
+    {
+        return $this->_oDb->getGroupsBy($aParams);          
+    }
+
+    public function getGroupCustom($aParams)
+    {
+        return $this->_oDb->getGroupCustom($aParams);
+    }
+
+    public function updateGroupCustom($aParamsSet, $aParamsWhere)
+    {
+        return $this->_oDb->updateGroupCustom($aParamsSet, $aParamsWhere);
+    }
+
+    public function deleteGroupCustom($aParamsWhere)
+    {
+        return $this->_oDb->deleteGroupCustom($aParamsWhere);
+    }
+
+    public function associateGroupCustomWithContent($iProfileId, $iContentId, $iGroupId)
+    {
+        return $this->updateGroupCustom(array('content_id' => $iContentId), array(
+            'profile_id' => $iProfileId,
+            'content_id' => 0,
+            'object' => $this->_sObject,
+            'group_id' => $iGroupId
+        ));
+    }
+
+    public function reassociateGroupCustomWithContent($iProfileId, $iContentId, $iGroupId)
+    {
+        $aGroupCustom = $this->getGroupCustom(array(
+            'type' => 'pco', 
+            'profile_id' => $iProfileId,
+            'content_id' => $iContentId,
+            'object' => $this->_sObject
+        ));
+
+        if(!empty($aGroupCustom) && is_array($aGroupCustom) && $aGroupCustom['group_id'] != $iGroupId) {
+            $this->deleteGroupCustom(array('id' => $aGroupCustom['id']));
+
+            $this->associateGroupCustomWithContent($iProfileId, $iContentId, $iGroupId);
+        }
+    }
+
+    public function deleteGroupCustomByContentId($iContentId)
+    {
+        return $this->_oDb->deleteGroupCustom(array('content_id' => $iContentId, 'object' => $this->_sObject));
+    }
+
+    public function deleteGroupCustomByProfileId($iProfileId)
+    {
+        $aGroups = $this->_oDb->getGroupCustom(array('type' => 'profile_id', 'profile_id' => $iProfileId));
+        if(empty($aGroups) || !is_array($aGroups))
+            return true;
+
+        foreach($aGroups as $aGroup)
+            $this->_oDb->deleteGroupCustom(array('id' => $aGroup['id']));
+
+        return true;
+    }
+
+    public function addDynamicGroups($aValues, $iOwnerId, $aParams)
+    {
+        if (isset($aParams['dynamic_groups']) && is_array($aParams['dynamic_groups']))
+            $aValues = array_merge($aValues, $aParams['dynamic_groups']);
+
+        return $aValues;
+    }
+
+    public function addAclGroups($aValues, $iOwnerId, $aParams)
+    {
+        $aCheck = checkActionModule($iOwnerId, 'show membership levels in privacy groups', 'system', false);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aValues;
+
+        $aLevels = BxDolAcl::getInstance()->getMemberships(false, true, true, true);
+        if(empty($aLevels) || !is_array($aLevels))
+            return $aValues;
+
+        $aValues[] = array('type' => 'group_header', 'value' => mb_strtoupper(_t('_sys_privacy_groups_acl')));
+        foreach($aLevels as $iId => $sTitle)
+            $aValues[] = array('key' => 'ml' . $iId, 'value' => $sTitle);
+        $aValues[] = array('type' => 'group_end');
+
+        return $aValues;
+    }
+
+    public function addSpaces($aValues, $iOwnerId, $aParams)
+    {
+        if (!$this->_aObject['spaces'])
+            return $aValues;
+
+        if (!($oProfile = BxDolProfile::getInstance($iOwnerId)))
+            return $aValues;
+
+        if (!($aModules = BxDolModuleQuery::getInstance()->getModules()))
+            return $aValues;
+        
+        $aExcludeModules = explode(',', getParam('sys_hide_post_to_context_for_privacy'));
+        
+        foreach ($aModules as $aModule) {
+            if (!$aModule['enabled'])
+                continue;
+
+            if ('all' != $this->_aObject['spaces'] && false === stripos($this->_aObject['spaces'], $aModule['name']))
+                continue;
+
+            if (!BxDolRequest::serviceExists($aModule['name'], 'act_as_profile'))
+                continue;
+            
+            if (in_array($aModule['name'], $aExcludeModules))
+                continue;
+
+            $oModule = BxDolModule::getInstance($aModule['name']);
+            
+            $a = BxDolService::call($aModule['name'], 'get_participating_profiles', array($oProfile->id()));
+            $aSpaces = array();       
+            foreach ($a as $iProfileId) {
+                if (!($o = BxDolProfile::getInstance($iProfileId)))
+                    continue;
+                $aSpaces[-$iProfileId] = array('key' => -$iProfileId, 'value' => $o->getDisplayName());
+            }
+
+            if ($aSpaces) {
+                $aItemStart = array(array('type' => 'group_header', 'value' => mb_strtoupper(BxDolService::call($aModule['name'], 'get_space_title'))));
+                $aItemEnd = array(array('type' => 'group_end'));
+                $aValues = array_merge($aValues, $aItemStart, array_values($aSpaces), $aItemEnd);
+            }
+        }
+        
+        return $aValues;
+    }
+    
+    /**
+     * Get database field name for action.
+     *
+     * @param  string $sObject privacy object name.
+     * @param  string $sAction action name.
+     * @return string with field name.
+     */
+    public static function getFieldName($sObject, $sAction = '')
+    {
+    	$oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return '';
+
+		if(empty($sAction))
+			$sAction = $oPrivacy->_aObject['action'];
+
+        return $oPrivacy->convertActionToField($sAction);
+    }
+
+    /**
+     * Get necessary condition array to use privacy in search classes
+     * @param $mixedGroupId group ID or array of group IDs
+     * @return array of conditions, for now with 'restriction' part only is returned
+     */
+    public function getContentByGroupAsCondition($mixedGroupId)
+    {
+        $aResult = array(
+            'restriction' => array (
+                'privacy_' . $this->_sObject => array(
+                    'value' => $mixedGroupId,
+                    'field' => $this->convertActionToField($this->_aObject['action']),
+                    'operator' => is_array($mixedGroupId) ? 'in' : '=',
+                    'table' => $this->_aObject['table'],
+                ),
+            ),
+        );      
+        bx_alert('system', 'privacy_condition', 0, false, array(
+            'group_id' => $mixedGroupId,
+            'field' => $this->convertActionToField($this->_aObject['action']),
+            'object' => $this->_aObject,
+            'privacy_object' => $this,
+            'result' => &$aResult
+            )
+        );
+        return $aResult;
+    }
+
+    /**
+     * Get necessary condition array to use privacy in search classes
+     * @param $iProfileIdOwner owner profile ID
+     * @return array of conditions, for now with 'restriction' part only is returned
+     */
+    public function getContentPublicAsCondition($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+        $mixedPrivacyGroups = $this->getPrivacyGroupsForContentPublic($iProfileIdOwner, $aCustomGroups);
+        if($mixedPrivacyGroups === true)
+        	return array();
+
+        return $this->getContentByGroupAsCondition($mixedPrivacyGroups);
+    }
+
+    /**
+     * Get necessary parts of SQL query to use privacy in other queries
+     * @param $mixedGroupId group ID or array of group IDs
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentByGroupAsSQLPart($mixedGroupId)
+    {
+        $sField = $this->convertActionToField($this->_aObject['action']);
+        return $this->_oDb->getContentByGroupAsSQLPart($sField, $mixedGroupId);
+    }
+
+	/**
+     * Get necessary parts of SQL query to use privacy in other queries
+     * @param $iProfileIdOwner owner profile ID
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentPublicAsSQLPart($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+        $mixedPrivacyGroups = $this->getPrivacyGroupsForContentPublic($iProfileIdOwner, $aCustomGroups);
+		if($mixedPrivacyGroups === true)
+        	return array();
+
+        return $this->getContentByGroupAsSQLPart($mixedPrivacyGroups);
+    }
+
+    /**
+     * Check whether the viewer can make requested action.
+     *
+     * @param  integer $iObjectId object ID the action to be performed with.
+     * @param  integer $iViewerId viewer ID.
+     * @return boolean result of operation.
+     */
+    public function check($iObjectId, $iViewerId = 0)
+    {
+        $aObject = $this->getObjectInfo($this->convertActionToField($this->_aObject['action']), $iObjectId);
+        $bRv = $this->_check($iObjectId, $iViewerId, $aObject);
+        bx_alert('system', 'check_privacy', 0, 0, array(
+           'object_id' => $iObjectId,
+           'viewer_id' => $iViewerId,
+           'object' => $aObject,
+           'object_privacy' => $this->_aObject,
+           'result' => &$bRv
+        ));
+        return $bRv;
+    }
+    
+    public function checkSpace($aObject, $iViewerId)
+    {
+        $oProfile = BxDolProfile::getInstance(-$aObject['group_id']);
+        if (!$oProfile)
+            return false;
+
+        return CHECK_ACTION_RESULT_ALLOWED === BxDolService::call($oProfile->getModule(), 'check_space_privacy', array($oProfile->getContentId()));
+    }
+
+    public function checkMeOnly($iOwnerId, $iViewerId)
+    {
+        return false;
+    }
+
+    public function checkPublic($iOwnerId, $iViewerId)
+    {
+        return true;
+    }
+
+    public function checkMembers($iOwnerId, $iViewerId)
+    {
+        return isMember();
+    }
+
+    public function checkFriends($iOwnerId, $iViewerId)
+    {
+        return BxDolConnection::getObjectInstance('sys_profiles_friends')->isConnected($iOwnerId, $iViewerId, true);
+    }
+
+    public function checkFriendsSelectedByObject($aObject, $iViewerId)
+    {
+        if(!$this->checkFriends($aObject['owner_id'], $iViewerId))
+            return false;
+
+        $aGroupCustom = $this->getGroupCustom(array(
+            'type' => 'pcog_ext', 
+            'profile_id' => $aObject['owner_id'], 
+            'content_id' => $aObject['id'], 
+            'object' => $this->_sObject, 
+            'group_id' => $aObject['group_id']
+        ));
+
+        return !empty($aGroupCustom['members']) && is_array($aGroupCustom['members']) && in_array($iViewerId, $aGroupCustom['members']);
+    }
+
+    public function checkRelations($iOwnerId, $iViewerId)
+    {
+        return BxDolConnection::getObjectInstance('sys_profiles_relations')->isConnected($iOwnerId, $iViewerId, true);
+    }
+
+    public function checkRelationsSelectedByObject($aObject, $iViewerId)
+    {
+        if(!$this->checkRelations($aObject['owner_id'], $iViewerId))
+            return false;
+
+        $aGroupCustom = $this->getGroupCustom(array(
+            'type' => 'pcog_ext', 
+            'profile_id' => $aObject['owner_id'], 
+            'content_id' => $aObject['id'], 
+            'object' => $this->_sObject, 
+            'group_id' => $aObject['group_id']
+        ));
+
+        return !empty($aGroupCustom['members']) && is_array($aGroupCustom['members']) && in_array($iViewerId, $aGroupCustom['members']);
+    }
+
+    public function checkCustomByObject($aObject, $iViewerId)
+    {
+        return '';
+    }
+
+    public function setTableFieldAuthor($sValue)
+    {
+        $this->_aObject['table_field_author'] = $sValue;
+
+        $this->_oDb->init($this->_aObject);
+    }
+
+    protected function getObjectInfo($sAction, $iObjectId)
+    {
+        return $this->_oDb->getObjectInfo($sAction, $iObjectId);
+    }
+
+	protected function getPrivacyGroupsForContentPublic($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+    	$aGroups = array(BX_DOL_PG_ALL);
+        if(isLogged()) {
+            $iProfileIdLogged = bx_get_logged_profile_id();
+            if($iProfileIdLogged == $iProfileIdOwner)
+                return true;
+
+            $aGroups[] = BX_DOL_PG_MEMBERS;
+            if($iProfileIdOwner && $this->checkFriends($iProfileIdOwner, $iProfileIdLogged))
+                $aGroups[] = BX_DOL_PG_FRIENDS;
+        }
+
+        return array_merge($aGroups, $aCustomGroups);
+    }
+
+    protected function getCheckMethod($s)
+    {
+        if(substr($s, 0, 1) != '@')
+            return false;
+
+        return bx_gen_method_name(str_replace('@', 'check_', $s));
+    }
+
+    protected function convertActionToField($sAction)
+    {
+    	return 'allow_' . strtolower(str_replace(' ', '-', $sAction)) . '_to';
+    }
+
+    /**
+     * Check whethere viewer is a member of dynamic group.
+     *
+     * @param  mixed   $mixedGroupId   dynamic group ID.
+     * @param  integer $iObjectOwnerId object owner ID.
+     * @param  integer $iViewerId      viewer ID.
+     * @return boolean result of operation.
+     */
+    protected function isDynamicGroupMember($mixedGroupId, $iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        return false;
+    }
+
+    /**
+     * get privacy groups for getGroupChooser
+     */ 
+    protected function getGroups() 
+    {
+        $aValues = array();
+        $aGroups = $this->_oDb->getGroupsBy(array('type' => 'active'));
+        foreach($aGroups as $aGroup) {
+            if((int)$aGroup['active'] == 0 || in_array($aGroup['id'], $this->_aGroupsExclude))
+               continue;
+
+            $aValues[] = array('key' => $aGroup['id'], 'value' => _t($aGroup['title']));
+        }
+        return $aValues;
+    }
+
+    /**
+     * Check whethere custom group is allowed in current circumstances.
+     * NOTE. Can be overwritten if it's needed.
+     * 
+     * @param type $aParams an array of parameters.
+     * @return boolean result of operation.
+     */
+    protected function isSelectGroupCustom($aParams)
+    {
+        return true;
+    }
+    
+    /**
+     * 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.
+     */
+    protected function _check($iObjectId, $iViewerId, $aObject)
+    {
+        if(empty($iViewerId))
+            $iViewerId = (int)bx_get_logged_profile_id();
+
+        if(empty($aObject) || !is_array($aObject))
+            return false;
+
+        if($aObject['group_id'] == BX_DOL_PG_HIDDEN)
+            return false;
+
+        $oViewer = BxDolProfile::getInstance($iViewerId);
+        if(($oViewer && isAdmin($oViewer->getAccountId())) || $iViewerId == $aObject['owner_id'])
+            return true;
+
+        if(strncmp($aObject['group_id'], 'ml', 2) === 0) {
+            $iLevel = (int)substr($aObject['group_id'], 2);
+            return (bool)BxDolAcl::getInstance()->isMemberLevelInSet(array($iLevel), $iViewerId);
+        }
+
+        if($aObject['group_id'] < 0)
+            return $this->checkSpace($aObject, $iViewerId);
+
+        $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((substr($sCheckMethod, -8) == 'ByObject' ? $aObject : $aObject['owner_id']), $iViewerId))
+                return true;
+        }
+
+        return $this->isDynamicGroupMember($aObject['group_id'], $aObject['owner_id'], $iViewerId, $iObjectId);
+    }
+}
+
+/** @} */

+ 615 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolProfile.php

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

+ 395 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolProfileQuery.php

@@ -0,0 +1,395 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * All queries related to profiles
+ */
+class BxDolProfileQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolProfileQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Get all account profiles.
+     * @param  string  $iAccountId account id
+     * @return profile array
+     */
+    public function getProfilesByAccount ($iAccountId)
+    {
+        if (!$iAccountId)
+            return array();
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+        $aAccountInfo = $oAccountQuery->getInfoById($iAccountId);
+
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? ORDER BY `id` = ? DESC", $iAccountId, $aAccountInfo['profile_id']);
+        return $this->getAllWithKey($sSql, 'id');
+    }
+
+    /**
+     * Get profile by content id, type and account.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentTypeAccount ($iContentId, $sType, $iAccountId)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? AND `type` = ? AND `content_id` = ?", $iAccountId, $sType, $iContentId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * Get profile by content id and type.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentAndType ($iContentId, $sType, $bClearCache = false)
+    {
+        if (!$iContentId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getProfileByContentAndType' . $iContentId . $sType;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `content_id` = ? AND `type` = ?", $iContentId, $sType);        
+        $mixedResult = $this->fromMemory($sKey, 'getRow', $sSql);
+        if (!$mixedResult)
+            $this->cleanMemory($sKey);
+        return $mixedResult;
+    }
+
+    public function getConnectedProfilesByType ($aSqlParts, $sType, $iStart, $iLimit)
+    {
+        if(empty($aSqlParts['join']))
+            return array();
+
+        $aBindings = array();
+
+        $sSelectClause = '';
+        if(!empty($aSqlParts['fields']))
+            foreach($aSqlParts['fields'] as $sName => $sField)
+                $sSelectClause .= ', ' . $sField;
+
+        $sWhereClause = '';
+        if(!empty($sType)) {
+            $sWhereClause = ' AND `sys_profiles`.`type`=:type';
+
+            $aBindings['type'] = $sType;
+        }
+
+        $sOrderClause = '';
+        if(!empty($aSqlParts['fields']['added']))
+            $sOrderClause = ' ORDER BY ' . $aSqlParts['fields']['added'] . ' DESC';
+
+        $sLimitClause = '';
+        if(!empty($iLimit))
+            $sLimitClause = $this->prepareAsString(' LIMIT ?, ?', $iStart, $iLimit);
+
+        return $this->getAllWithKey("SELECT `sys_profiles`.*" . $sSelectClause . " FROM `sys_profiles` " . $aSqlParts['join'] . " WHERE 1" . $sWhereClause . $sOrderClause . $sLimitClause, 'id', $aBindings);
+    }
+
+    /**
+     * 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, $bClearCache = false)
+    {
+        if (!$iAccountId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getCurrentProfileByAccount' . $iAccountId;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+
+        $sSql = $this->prepare("SELECT `profile_id` FROM `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON (`a`.`profile_id` = `p`.`id`) WHERE `a`.`id` = ? LIMIT 1", $iAccountId);        
+        $iProfileId = $this->fromMemory($sKey, 'getOne', $sSql);
+        if (!$iProfileId) {
+            $this->cleanMemory($sKey);
+            $sSql = $this->prepare("SELECT `id` FROM `sys_profiles` WHERE `account_id` = ? ORDER BY FIELD(`type`, 'system', 'bx_organizations', 'bx_persons') DESC 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->fromMemory('sys_profiles_' . $sField . '_' . $sValue, 'getRow', $sSql);
+    }
+
+    /**
+     * get profile id by id
+     */
+    public function getIdById($iId, $bClearCache = false)
+    {
+        if (!$iId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getIdById' . $iId;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+        
+        $sSql = $this->prepare("SELECT `id` FROM `sys_profiles` WHERE `id` = ? LIMIT 1", $iId);
+        $mixedResult = $this->fromMemory($sKey, 'getOne', $sSql);
+        if (!$mixedResult)
+            $this->cleanMemory($sKey);
+        return $mixedResult;
+    }
+
+    /**
+     * get account info by profile id
+     */
+    public function getAccountInfoByProfileId($iId)
+    {
+        $sSql = $this->prepare("SELECT `a`.* FROM `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON (`p`.`account_id` = `a`.`id`) WHERE `p`.`id` = ? LIMIT 1", $iId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * get account email by profile id
+     */
+    public function getEmailById($iId)
+    {
+        $a = $this->getAccountInfoByProfileId($iId);
+        if (!$a || empty($a['email']))
+            return false;
+        return $a['email'];
+    }
+
+    /**
+     * Get profile info by id
+     * @param  int   $iID profile id
+     * @return array with profile info
+     */
+    public function getInfoById( $iID )
+    {
+        return $this->_getDataByField('id', (int)$iID);
+    }
+
+	/**
+     * Is profile online by id
+     * @param  int $iId profile id
+     * @return profile online status
+     */
+    public function isOnline($iId)
+    {
+        $sSql = $this->prepare("SELECT 
+        		`tp`.`id` 
+        	FROM `sys_profiles` AS `tp` 
+        	INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id` 
+        	WHERE 
+        		`tp`.`id` = ? AND 
+        		`ta`.`profile_id`=`tp`.`id` AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?) 
+        	LIMIT 1", $iId, (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql) > 0;
+    }
+    
+    /**
+     * Get online profiles count 
+     * @return online profiles count 
+     */
+    public function getOnlineCount()
+    {
+        $sSql = $this->prepare("SELECT 
+        		COUNT(`tp`.`id`) 
+        	FROM `sys_profiles` AS `tp` 
+        	INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id` 
+        	WHERE 
+        		`ta`.`profile_id`=`tp`.`id` AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?)", (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql);
+    }
+
+    public function getProfileQuota($iProfileId)
+    {
+        if (!$iProfileId) // for guests and storages without owner don't count per-user quota
+            return array('current_size' => 0, 'current_number' => 0, 'quota_size' => 0, 'quota_number' => 0, 'max_file_size' => 0);
+
+        $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 updateProfileQuota($iProfileId, $iSize, $iNumber = 1)
+    {
+        if (!$iProfileId) // for guests and storages without owner don't update per-user quota
+            return true;
+
+        $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;
+    }
+
+    /**
+     * 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)
+    {
+        $aInfo = $this->getInfoById($iID);
+        $sSql = $this->prepare("DELETE FROM `sys_profiles` WHERE `id` = ? LIMIT 1", $iID);
+        if ($res = $this->query($sSql)) {
+            $this->getProfileByContentAndType($aInfo['content_id'], $aInfo['type'], true);
+            $this->getCurrentProfileByAccount($aInfo['account_id'], true);
+            $this->getIdById($iID, true);
+        }
+        return $res;
+    }
+
+    /**
+     * Reset deleted profile ids and assign system profile ids.
+     * Should be called after profiles module deletion.
+     * It can be called automatically if 
+     * @code
+     * 'process_deleted_profiles' => 1 
+     * @endcode
+     * is specified in module config.php file in 'uninstall' section.
+     */
+    public function processDeletedProfiles ()
+    {
+        $bResult = true;
+
+        // reset deleted profiles
+        $bResult &= $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") !== false;
+
+        // try to assign another non-system profile to reset accounts
+        $bResult &= $this->query("UPDATE `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON `a`.`id`=`p`.`account_id` AND `p`.`type`<>'system' AND `a`.`profile_id`='0' SET `a`.`profile_id`=`p`.`id`") !== false;
+
+        // assign system profile to reset accounts
+        $bResult &= $this->query("UPDATE `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON `a`.`id`=`p`.`account_id` AND `p`.`type`='system' AND `a`.`profile_id`='0' SET `a`.`profile_id`=`p`.`id`") !== false;
+
+        return $bResult;
+    }
+}
+
+/** @} */

+ 174 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolPush.php

@@ -0,0 +1,174 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolPush extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_sAppId;
+    protected $_sRestApi;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_sAppId = getParam('sys_push_app_id');
+        $this->_sRestApi = getParam('sys_push_rest_api');
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolPush();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+    
+    /**
+     * Get tags to send to PUSH server
+     * @param $iProfileId - profile ID
+     * @return array of tags
+     */
+    public static function getTags($iProfileId = false)
+    {
+        if (false === $iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oAccount = $oProfile ? $oProfile->getAccountObject() : null;
+        if (!$oProfile || !$oAccount)
+            return false;
+
+        $sEmail = $oAccount->getEmail();
+        $a = array (
+            'user' => $iProfileId,
+            'user_hash' => encryptUserId($iProfileId),
+            'real_name' => $oProfile->getDisplayName(),
+            'email' => $sEmail,
+            'email_hash' => $sEmail ? hash_hmac('sha256', $sEmail, getParam('sys_push_app_id')) : '',
+        );
+
+        bx_alert('system', 'push_tags', $iProfileId, $iProfileId, array('tags' => &$a));
+
+        return $a;
+    }
+
+    /**
+     * @param $a - array to fill with notification counter per module
+     * @return total number of notifications
+     */
+    public static function getNotificationsCount($iProfileId = 0, &$aBubbles = null)
+    {    
+        $iMemberIdCookie = null;
+        $bLoggedMemberGlobals = null;
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) {            
+            if (!empty($_COOKIE['memberID']))
+                $iMemberIdCookie = $_COOKIE['memberID'];
+            if (!empty($GLOBALS['logged']['member']))
+                $bLoggedMemberGlobals = $GLOBALS['logged']['member'];
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            $_COOKIE['memberID'] = $oProfile ? $oProfile->getAccountId() : 0;
+            $GLOBALS['logged']['member'] = $oProfile ? true : false;
+        }
+    
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id())
+            unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']);
+        $oMenu = BxDolMenu::getObjectInstance('sys_account_notifications'); // sys_toolbar_member
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id())
+            unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']);
+
+        $a = $oMenu->getMenuItems();
+        $iBubbles = 0;
+        foreach ($a as $r) {
+            if (!$r['bx_if:addon']['condition'])
+                continue;
+            if (null !== $aBubbles)
+                $aBubbles[$r['name']] = $r['bx_if:addon']['content']['addon'];
+            $iBubbles += $r['bx_if:addon']['content']['addon'];
+        }
+
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) {
+            if (null === $iMemberIdCookie)
+                unset($_COOKIE['memberID']);
+            else
+                $_COOKIE['memberID'] = $iMemberIdCookie;
+
+            if (null === $bLoggedMemberGlobals)
+                unset($GLOBALS['logged']['member']);
+            else
+                $GLOBALS['logged']['member'] = $bLoggedMemberGlobals;
+        }
+
+        return $iBubbles;
+    }
+
+    public function send($iProfileId, $aMessage, $bAddToQueue = false)
+    {
+        if(empty($this->_sAppId) || empty($this->_sRestApi))
+            return false;
+
+        if($bAddToQueue && BxDolQueuePush::getInstance()->add($iProfileId, $aMessage))
+            return true;
+
+        $iBadgeCount = $this->getNotificationsCount($iProfileId);
+    
+		$aFields = array(
+			'app_id' => $this->_sAppId,
+			'filters' => array(
+		        array('field' => 'tag', 'key' => 'user_hash', 'relation' => '=', 'value' => encryptUserId($iProfileId))
+            ),
+			'contents' => !empty($aMessage['contents']) && is_array($aMessage['contents']) ? $aMessage['contents'] : array(),
+			'headings' => !empty($aMessage['headings']) && is_array($aMessage['headings']) ? $aMessage['headings'] : array(),
+            'web_url' => !empty($aMessage['url']) ? $aMessage['url'] : '',
+            'data' => array('url' => !empty($aMessage['url']) ? $aMessage['url'] : ''),
+            'chrome_web_icon' => !empty($aMessage['icon']) ? $aMessage['icon'] : '',
+            'ios_badgeType' => 'SetTo',
+            'ios_badgeCount' => $iBadgeCount,
+		);
+
+		$oChannel = curl_init();
+		curl_setopt($oChannel, CURLOPT_URL, "https://onesignal.com/api/v1/notifications");
+		curl_setopt($oChannel, CURLOPT_HTTPHEADER, array(
+			'Content-Type: application/json; charset=utf-8',
+			'Authorization: Basic ' . $this->_sRestApi
+		));
+		curl_setopt($oChannel, CURLOPT_RETURNTRANSFER, true);
+		curl_setopt($oChannel, CURLOPT_HEADER, false);
+		curl_setopt($oChannel, CURLOPT_POST, true);
+		curl_setopt($oChannel, CURLOPT_POSTFIELDS, json_encode($aFields));
+		curl_setopt($oChannel, CURLOPT_SSL_VERIFYPEER, false);
+
+		$sResult = curl_exec($oChannel);
+		curl_close($oChannel);
+        
+        $oResult = @json_decode($sResult, true);
+        if(isset($oResult['errors'])){
+            foreach($oResult['errors'] as $sError) {  
+                bx_log('sys_push', $sError . " Message:" . json_encode($aMessage));
+            }
+        }
+
+		return $sResult;
+    }
+}
+
+/** @} */

+ 256 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolReport.php

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

+ 87 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolReportQuery.php

@@ -0,0 +1,87 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolReport
+ */
+class BxDolReportQuery extends BxDolObjectQuery
+{
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $this->_sMethodGetEntry = 'getReport';
+    }
+
+    public function getSqlParts($sMainTable, $sMainField)
+    {
+    	$aResult = parent::getSqlParts($sMainTable, $sMainField);
+        if(empty($aResult))
+            return $aResult;
+
+		$aResult['fields'] = ", `{$this->_sTable}`.`count` as `report_count` ";
+        return $aResult;
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+    	/*
+    	 * 'false' is returned everytime to allow multiple reports for everybody.
+    	 */
+        return false;
+    }
+
+    public function getPerformedBy($iObjectId, $iStart = 0, $iPerPage = 0)
+    {
+        $sLimitClause = "";
+        if(!empty($iPerPage))
+            $sLimitClause = $this->prepareAsString(" LIMIT ?, ?", $iStart, $iPerPage);
+
+        $sQuery = $this->prepare("SELECT `author_id`, `type`, `text` FROM `{$this->_sTableTrack}` WHERE `object_id`=? ORDER BY `date` DESC" . $sLimitClause, $iObjectId);
+        return $this->getAll($sQuery);
+    }
+
+    public function getReport($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `count` as `count` FROM {$this->_sTable} WHERE `object_id` = ? LIMIT 1", $iObjectId);
+
+        $aResult = $this->getRow($sQuery);
+        if(empty($aResult) || !is_array($aResult))
+            $aResult = array('count' => 0);
+
+        return $aResult;
+    }
+
+	public function putReport($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `object_id` FROM `{$this->_sTable}` WHERE `object_id` = ? LIMIT 1", $iObjectId);
+        $bExists = (int)$this->getOne($sQuery) != 0;
+
+        if(!$bExists)
+            $sQuery = $this->prepare("INSERT INTO `{$this->_sTable}` SET `object_id` = ?, `count` = '1'", $iObjectId);
+        else
+            $sQuery = $this->prepare("UPDATE `{$this->_sTable}` SET `count` = `count` + 1 WHERE `object_id` = ?", $iObjectId);
+
+        return (int)$this->query($sQuery) != 0;
+    }
+    
+    public function clearReports($iObjectId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTable}` WHERE `object_id` = ?", $iObjectId);
+        $this->query($sQuery);
+        
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTableTrack}` WHERE `object_id` = ?", $iObjectId);
+        $this->query($sQuery);
+        
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = 0 WHERE `{$this->_sTriggerFieldId}` = ?", $iObjectId);
+        $this->query($sQuery);
+    }
+}
+
+/** @} */

+ 1075 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolSearch.php

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

+ 62 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolSessionQuery.php

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

+ 1442 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorage.php

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

+ 366 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageQuery.php

@@ -0,0 +1,366 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for 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->fromCache('sys_objects_storage_' . $sObject, 'getRow', $sQuery);
+    }
+
+    static public function getStorageObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_objects_storage`";
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function getAllGhosts ($aParams)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_storage_ghosts` WHERE 2>1 ";
+        if (isset($aParams['profile_id']))
+            $sQuery .= " AND profile_id = :profile_id";
+        if (isset($aParams['object']))
+            $sQuery .= " AND object = :object";
+        if (isset($aParams['content_id']))
+            $sQuery .= " AND content_id = :content_id";
+        if (isset($aParams['sort_field'])){
+			$sQuery .= " ORDER BY `" . $aParams['sort_field'] . "` " . (isset($aParams['sort_direction']) ? $aParams['sort_direction'] : '');
+			unset($aParams['sort_field']);
+			unset($aParams['sort_direction']);
+		}
+        return $oDb->getAll($sQuery, $aParams);
+    }
+    
+    public function changeStorageEngine ($sEngine)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_objects_storage` SET `engine` = ? WHERE `object` = ?", $sEngine, $this->_aObject['object']);
+        return $this->query($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 getIconFontByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `icon_font` 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 = '', $sIconFont = '')
+    {
+        $sQuery = $this->prepare("INSERT IGNORE INTO `sys_storage_mime_types` SET `ext` = ?, `mime_type` = ?, `icon` = ?, `icon_font` = ?", $sExt, $sMimeType, $sIcon, $sIconFont);
+        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)
+    {
+        return BxDolProfileQuery::getInstance()->getProfileQuota($iProfileId);
+    }
+
+    public function updateUserQuota($iProfileId, $iSize, $iNumber = 1)
+    {
+        return BxDolProfileQuery::getInstance()->updateProfileQuota($iProfileId, $iSize, $iNumber);
+    }
+
+    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)
+    {
+    	$aBindings = array(
+    		$sField => $sValue,
+    		'id' => $iFileId
+    	);
+    	
+        $sAdd = '';
+        if ($isUpdateModifiedField) {
+        	$aBindings['modified'] = time();
+
+            $sAdd = ", `modified` = :modified";
+        }
+
+        $sQuery = "UPDATE " . $this->_sTableFiles . " SET `{$sField}` = :{$sField} {$sAdd} WHERE `id` = :id";
+        return $this->query($sQuery, $aBindings);
+    }
+
+    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, $aProfiles = array(), $isAdmin = false)
+    {
+        $aBindings = array(
+            'content' => $iContentId,
+    		'profile' => $iProfileId,
+    		'object' => $this->_aObject['object']
+        );
+
+        $sSetAddon = '';
+        $sWhere = " AND `profile_id` = :profile ";
+        if ($aProfiles || $isAdmin) {
+            $sSetAddon = ', `profile_id` = :profile ';
+            $sWhere = $isAdmin ? "" : " AND `profile_id` IN(" . $this->implode_escape($aProfiles) . ")";
+        }
+        
+        $sQuery = $this->prepare("UPDATE `sys_storage_ghosts` SET `content_id` = :content $sSetAddon WHERE `object` = :object $sWhere AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")");
+        return $this->res($sQuery, $aBindings);
+    }
+
+    public function deleteGhosts($mixedFileIds, $iProfileId, $iContentId = false)
+    {
+    	$aBindings = array(
+    		'profile_id' => $iProfileId,
+    		'object' => $this->_aObject['object']
+    	);
+        $sQuery = "DELETE FROM `sys_storage_ghosts` WHERE `profile_id` = :profile_id AND `object` = :object AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")";
+        if (false !== $iContentId) {
+        	$aBindings['content_id'] = $iContentId;
+
+            $sQuery .= " AND `content_id` = :content_id";
+        }
+        $iCount = $this->query($sQuery, $aBindings);
+        if ($iCount)
+            $this->query("OPTIMIZE TABLE `sys_storage_ghosts`");
+        return $iCount;
+    }
+
+    public function getGhosts($mixedProfileId, $iContentId = false, $isAdmin = false)
+    {
+        return $this->getFiles($mixedProfileId, true, $iContentId, $isAdmin);
+    }
+
+    public function getGhost($iFileId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_storage_ghosts` WHERE `object` = ? AND `id` = ?", $this->_aObject['object'], $iFileId);
+        return $this->getRow($sQuery);
+    }
+
+    public function getFiles($mixedProfileId, $isGhostsOnly = false, $iContentId = false, $isAdmin = false)
+    {
+        $aBindings = array();        
+
+        $sWhere = '';
+        $sJoin = '';
+        if ($isGhostsOnly) {
+            $aBindings['object'] = $this->_aObject['object'];
+
+            $sOnProfile = '';
+            if ($isAdmin && $iContentId) {
+                // don't check profile id for admins, so admin can edit any entry
+            }
+            elseif (is_array($mixedProfileId) && $mixedProfileId) {
+                $sOnProfile = " AND `g`.`profile_id` IN(" . $this->implode_escape($mixedProfileId) . ") ";
+            } 
+            elseif (!is_array($mixedProfileId) && false !== $mixedProfileId) {
+                $aBindings['profile_id'] = $mixedProfileId;
+                
+                $sOnProfile = " AND `g`.`profile_id` = :profile_id ";                
+            }
+            
+            $sJoin .= " INNER JOIN `sys_storage_ghosts` AS `g` ON (`f`.`id` = `g`.`id` AND `g`.`object` = :object " . $sOnProfile;
+            if (false !== $iContentId) {
+                $aBindings['content_id'] = $iContentId;
+
+                $sJoin .= " AND `g`.`content_id` = :content_id";
+            }
+
+            $sJoin .= ')';
+        } 
+        else {
+            if (is_array($mixedProfileId) && $mixedProfileId) {
+                $sWhere = " WHERE `f`.`profile_id` IN(" . $this->implode_escape($mixedProfileId) . ") ";
+            }
+            elseif (!is_array($mixedProfileId) && false !== $mixedProfileId) {
+                $aBindings['profile_id'] = $mixedProfileId;
+
+                $sWhere = " WHERE `f`.`profile_id` = :profile_id ";
+            }
+        }
+
+        $sQuery = "SELECT `f`.* FROM " . $this->_sTableFiles . " AS `f` " . $sJoin . $sWhere;
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    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);
+    }
+}
+
+/** @} */

+ 242 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3.php

@@ -0,0 +1,242 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+if (!defined('CURL_SSLVERSION_TLSv1'))
+    define('CURL_SSLVERSION_TLSv1', 1);
+
+/**
+ * File storage in Amazon S3.
+ * @see BxDolStorage
+ */
+class BxDolStorageS3 extends BxDolStorage
+{
+    protected $_s3;
+    protected $_sBucket;
+    protected $_sDomain;
+    protected $_sEndpoint;
+    protected $_bSSL;
+    protected $_bReducedRedundancy;
+
+    /**
+     * constructor
+     */
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_bSSL = (isset($this->_aParams['ssl']) && $this->_aParams['ssl']) || (!isset($this->_aParams['ssl']) && strncmp(BX_DOL_URL_ROOT, 'https://', 8) === 0) ? true : false;
+
+        $this->_sEndpoint = getParam('sys_storage_s3_endpoint');
+        $this->_sBucket = getParam('sys_storage_s3_bucket');
+        $this->_sDomain = getParam('sys_storage_s3_domain');
+        $this->_bReducedRedundancy = isset($this->_aParams['reduced_redundancy']) && $this->_aParams['reduced_redundancy'] ? true : false;
+
+        $this->init($aObject);
+    }
+
+    protected function init ($aObject)
+    {
+        require_once(BX_DIRECTORY_PATH_PLUGINS . 'amazon-s3/S3.php');
+        $this->_s3 = new S3(
+            getParam('sys_storage_s3_access_key'), 
+            getParam('sys_storage_s3_secret_key'), 
+            $this->_bSSL, 
+            $this->_sEndpoint ? $this->_sEndpoint : 's3.amazonaws.com'
+        );
+    }
+
+    /**
+     * Get file url.
+     * @param $iFileId file
+     * @return file url
+     */
+    public function getFileUrlById($iFileId)
+    {
+        $aFile = $this->_oDb->getFileById($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($aFile['private']) {
+            $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+
+            if ($this->_sDomain)
+                return $this->_s3->getAuthenticatedURL($this->_sDomain, $sFileLocation, $this->_aObject['token_life'], true, $this->_bSSL);
+            else
+                return $this->_s3->getAuthenticatedURL($this->_sBucket, $sFileLocation, $this->_aObject['token_life'], false, $this->_bSSL);
+        }
+
+        return $this->getObjectBaseUrl($aFile['private']) . $aFile['path'];
+    }
+
+    /**
+     * Start file fownloading by remote id. If file is private then token is checked.
+     */
+    public function download ($sRemoteId, $sToken = false)
+    {
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $aFile = $this->_oDb->getFileByRemoteId($sRemoteId);
+        if (!$aFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        if ($aFile['private'] && !$this->_oDb->isTokenValid($aFile['id'], $sToken)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_PERMISSION_DENIED);
+            return false;
+        }
+
+        $sUrl = $this->getFileUrlById($aFile['id']);
+        header("Location: " . $sUrl);
+
+        return parent::download($aFile);
+    }
+    
+    /**
+     * Set file private or public.
+     */
+    public function setFilePrivate($iFileId, $isPrivate = true)
+    {
+        $aFile = $this->_oDb->getFileById($iFileId);
+        $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+        if (($aACP = $this->_s3->getAccessControlPolicy($this->_sBucket, $sFileLocation)) === false) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+            return false;
+        }
+
+        if (!is_array($aACP['acl']) || !$aACP['acl']) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+            return false;
+        }
+
+        // check current permissions
+        $aNewACP = $aACP;
+        unset($aNewACP['acl']);
+        $aNewACP['acl'] = array();
+        $aGroupPublic = false;
+        $aGroupPrivate = false;
+        foreach ($aACP['acl'] as $r) {
+            if ('Group' == $r['type']) {
+                if (isset($r['uri']) && $r['uri'] == 'http://acs.amazonaws.com/groups/global/AllUsers')
+                    $aGroupPublic = $r;
+                elseif (isset($r['uri']) && $r['uri'] == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers')
+                    $aGroupPrivate = $r;
+                else
+                    $aNewACP['acl'][] = $r;
+            } else {
+                $aNewACP['acl'][] = $r;
+            }
+        }
+
+        // determine permissions changing
+
+        $aGroupAdd = false;
+
+        if ($isPrivate && (!$aGroupPrivate || $aGroupPublic)) {
+
+            // make private
+            $aGroupAdd = array (
+                    'type' => 'Group',
+                    'uri' => 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers',
+                    'permission' => 'READ',
+                );
+
+        } elseif (!$isPrivate && ($aGroupPrivate || !$aGroupPublic)) {
+
+            // make public
+            $aGroupAdd = array (
+                    'type' => 'Group',
+                    'uri' => 'http://acs.amazonaws.com/groups/global/AllUsers',
+                    'permission' => 'READ',
+                );
+
+        }
+
+        // change permission if necessary
+
+        if ($aGroupAdd) {
+            $aNewACP['acl'][] = $aGroupAdd;
+            if (!$this->_s3->setAccessControlPolicy($this->_sBucket, $sFileLocation, $aNewACP)) {
+                $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+                return false;
+            }
+        }
+
+        return parent::setFilePrivate($iFileId, $isPrivate);
+    }
+
+    // ----------------
+
+    protected function generateHeaders($sFileName, $isPrivate, $sMimeType = '')
+    {
+        if (!$sMimeType)
+            $sMimeType = $this->getMimeTypeByFileName($sFileName);
+
+        $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),
+            ));
+        }
+    
+        return $aRequestHeaders;
+    }
+
+    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 : '');
+        $aRequestHeaders = $this->generateHeaders($sName, $isPrivate);
+        $aMetaHeaders = array();
+
+        $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 . '.' . ($this->_sEndpoint ? $this->_sEndpoint : 's3.amazonaws.com'). '/' . $this->getObjectBaseDir($isPrivate);
+    }
+}
+
+/** @} */

+ 29 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3v4.php

@@ -0,0 +1,29 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * File storage in Amazon S3 with signature v4 authorisation.
+ * @see BxDolStorage
+ */
+class BxDolStorageS3v4 extends BxDolStorageS3
+{
+    protected function init ($aObject)
+    {
+        require_once(BX_DIRECTORY_PATH_PLUGINS . 'amazon-s3/S3v4.php');
+        $this->_s3 = new S3v4\S3(
+            getParam('sys_storage_s3_access_key'), 
+            getParam('sys_storage_s3_secret_key'), 
+            $this->_bSSL, 
+            $this->_sEndpoint ? $this->_sEndpoint : 's3.amazonaws.com',
+            getParam('sys_storage_s3_region')
+        );
+    }
+}
+
+/** @} */

+ 168 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolStorageS3v4alt.php

@@ -0,0 +1,168 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+if (!defined('CURL_SSLVERSION_TLSv1'))
+    define('CURL_SSLVERSION_TLSv1', 1);
+
+if (!defined('BX_DOL_STORAGE_S3V4_MULTIPART_UPLOAD'))
+    define('BX_DOL_STORAGE_S3V4_MULTIPART_UPLOAD', 5*1024*1024);
+
+define('AKEEBAENGINE', 1);
+
+/**
+ * Alternatove file storage implementation for Amazon S3 
+ * compatible storge with Signature v4 support.
+ * @see BxDolStorage
+ */
+class BxDolStorageS3v4alt extends BxDolStorageS3
+{
+    protected function init ($aObject)
+    {
+        $oConfiguration = new Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
+            getParam('sys_storage_s3_access_key'),
+            getParam('sys_storage_s3_secret_key'),
+            getParam('sys_storage_s3_sig_ver'),
+            getParam('sys_storage_s3_region')
+        );
+        if ($this->_sEndpoint)
+            $oConfiguration->setEndpoint($this->_sEndpoint);
+
+        $this->_s3 = new Akeeba\Engine\Postproc\Connector\S3v4\Connector($oConfiguration);
+    }
+
+    /**
+     * 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'];
+            $sRet = $this->_s3->getAuthenticatedURL($this->_sBucket, $sFileLocation, $this->_aObject['token_life'], $this->_bSSL);
+        } 
+        else {
+            $sRet = $this->getObjectBaseUrl($aFile['private']) . $aFile['path'];
+        }
+    
+        if ('s3.wasabisys.com' === $this->_sEndpoint)
+            $sRet = str_replace('//s3.', '//' . $this->_sBucket . '.s3.', $sRet);
+
+        return $sRet;
+    }
+
+    public function setFilePrivate($iFileId, $isPrivate = true)
+    {
+        // since in S3v4 lib this feature not implemented then we need to get file and upload it back with new ACL
+
+        $aFile = $this->_oDb->getFileById($iFileId);
+        if (!$aFile)
+            return false;
+
+        $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+        
+        $sTmpFile = tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']);
+        if (!$sTmpFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILESYSTEM_PERM);
+            return false;
+        }
+
+        try {
+            $this->_s3->getObject($this->_sBucket, $sFileLocation, $sTmpFile);
+        } catch (Exception $e) {
+            @unlink($sTmpFile);
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK);
+            return false;
+        }
+
+        $sACL = $isPrivate ? Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_AUTHENTICATED_READ : Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_PUBLIC_READ;
+        $aRequestHeaders = $this->generateHeaders('', $isPrivate, $aFile['mime_type']);
+        if (!$this->_upload($sTmpFile, $sFileLocation, $sACL, $aRequestHeaders))
+            return false;
+
+        return BxDolStorage::setFilePrivate($iFileId, $isPrivate);
+    }
+    
+    // ----------------
+
+    protected function addFileToEngine($sTmpFile, $sLocalId, $sName, $isPrivate, $iProfileId)
+    {
+        $sExt = $this->getFileExt($sName);
+        $sPath = $this->genPath($sLocalId, $this->_aObject['levels']);
+        $sRemoteNamePath = $sPath . $sLocalId . ($sExt ? '.' . $sExt : '');
+        $aRequestHeaders = $this->generateHeaders($sName, $isPrivate);
+        $sACL = $isPrivate ? Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_AUTHENTICATED_READ : Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_PUBLIC_READ;
+
+        return $this->_upload($sTmpFile, $this->getObjectBaseDir($isPrivate) . $sRemoteNamePath, $sACL, $aRequestHeaders);
+    }
+
+    protected function _upload($sInputFile, $sUri, $sACL, $aRequestHeaders)
+    {
+        try {
+            $oInputFile = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile);
+            if (filesize($sInputFile) < BX_DOL_STORAGE_S3V4_MULTIPART_UPLOAD) {
+                $this->_s3->putObject($oInputFile, $this->_sBucket, $sUri, $sACL, $aRequestHeaders);
+            }
+            else {
+                $sUploadSessionId = $this->_s3->startMultipart($oInputFile, $this->_sBucket, $sUri, $sACL, $aRequestHeaders);
+
+                $sETags = array();
+                $sETag = null;
+                $iPartNumber = 0;
+
+                do
+                {
+                    // IMPORTANT: You MUST create the input afresh before each uploadMultipart call
+                    $oInput = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile);
+                    $oInput->setUploadID($sUploadSessionId);
+                    $oInput->setPartNumber(++$iPartNumber);
+                    
+                    $sETag = $this->_s3->uploadMultipart($oInput, $this->_sBucket, $sUri);
+
+                    if (!is_null($sETag))
+                        $sETags[] = $sETag;
+                }
+                while (!is_null($sETag));
+
+                // IMPORTANT: You MUST create the input afresh before finalising the multipart upload
+                $oInput = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile);
+                $oInput->setUploadID($sUploadSessionId);
+                $oInput->setEtags($sETags);
+
+                $this->_s3->finalizeMultipart($oInput, $this->_sBucket, $sUri);
+            }
+
+        } catch (Exception $e) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function deleteFileFromEngine($sFilePath, $isPrivate)
+    {
+        $sFileLocation = $this->getObjectBaseDir($isPrivate) . $sFilePath;
+
+        try {
+            $this->_s3->deleteObject($this->_sBucket, $sFileLocation);
+        } catch (Exception $e) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK);
+            return false;
+        }
+
+        return true;
+    }
+}
+
+/** @} */

+ 3027 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTemplate.php

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

+ 312 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTranscoderQuery.php

@@ -0,0 +1,312 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_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 ($sFilterName = '')
+    {
+        $aBindings = array('object' => $this->_aObject['object']);
+        $sWhere = '';
+        if ($sFilterName) {
+            $sWhere .= ' AND `filter` = :filter '; 
+            $aBindings['filter'] = $sFilterName;
+        }
+        $sQuery = "SELECT * FROM {$this->_sTableFilters} WHERE `transcoder_object` = :object $sWhere ORDER BY `order` ASC";
+        return $this->fromMemory('BxDolTranscoderQuery::getTranscoderFilters' . $this->_aObject['object'] . $sFilterName, 'getAll', $sQuery, $aBindings);
+
+    }
+
+    public function updateHandler ($iFileId, $mixedHandler)
+    {
+    	$aBindings = array(
+    		'transcoder' => $this->_aObject['object'],
+    		'file_id' => $iFileId,
+    		'handler' => $mixedHandler
+    	);
+
+        $sUpdateATime = '';
+        if ($this->_aObject['atime_tracking']) {
+        	$aBindings['atime'] = time();
+
+            $sUpdateATime = ", `atime` = :atime";
+        }
+
+        return $this->query("INSERT INTO {$this->_sTableFiles} SET `transcoder_object` = :transcoder, `file_id` = :file_id, `handler` = :handler $sUpdateATime ON DUPLICATE KEY UPDATE `file_id` = :file_id $sUpdateATime", $aBindings);
+    }
+
+    public function getFileIdByHandler ($mixedHandler)
+    {
+        $sQuery = $this->prepare("SELECT `file_id` FROM {$this->_sTableFiles} WHERE `transcoder_object` = ? AND `handler` = ? LIMIT 1", $this->_aObject['object'], (string)$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` = ? LIMIT 1", $iTime, $this->_aObject['object'], (string)$mixedHandler);
+        $mixedRes = $this->res($sQuery);
+        $this->setReadOnlyMode(true);
+        return $mixedRes;
+    }
+
+    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'])
+            return array();
+
+        $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 deleteFromQueueById ($iId)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `" . self::TABLE_QUEUE . "` WHERE `id` = ?", $iId);
+        return $oDb->query($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;
+
+		$aSet = array(
+			'status' => $sStatus,
+			'changed' => time()
+		);
+
+		if($sLog)
+			$aSet['log'] = $sLog;
+        if($sServer)
+        	$aSet['server'] = $sServer;
+		if($mixedIdResult && $sFileUrlResult)
+			$aSet = array_merge($aSet, array(
+				'file_id_result' => $mixedIdResult,
+				'file_url_result' => $sFileUrlResult,
+				'file_ext_result' => $sFileExtResult
+			));
+
+        $sQuery = $this->prepare("UPDATE `" . $this->_sTableQueue . "` SET " . $this->arrayToSQL($aSet) . " WHERE `transcoder_object` = ? AND `file_id_source` = ?", $this->_aObject['object'], $mixedId);
+        return $this->query($sQuery);
+    }
+
+    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` = ? LIMIT 1", $this->_aObject['object'], (string)$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` = ? LIMIT 1", $sData, $this->_aObject['object'], (string)$mixedHandler);
+
+        return $this->res($sQuery);
+    }
+}
+
+/** @} */

+ 77 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolTwilio.php

@@ -0,0 +1,77 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolTwilio extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_sSid;
+    protected $_sToken;
+    protected $_sFromNumber;
+    
+    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->_sSid = getParam('sys_twilio_gate_sid');
+        $this->_sToken = getParam('sys_twilio_gate_token');
+        $this->_sFromNumber = getParam('sys_twilio_gate_from_number');
+    }
+
+    /**
+     * 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 BxDolTwilio();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+    
+    public function sendSms($sTo, $sMessage, $sFrom = '')
+    {
+        try{
+            require_once BX_DIRECTORY_PATH_PLUGINS . 'Twilio/autoload.php'; // Loads the library
+            $client = new Twilio\Rest\Client($this->_sSid, $this->_sToken);
+            $aParams = array('body' => $sMessage, 'from' => $sFrom != '' ? $this->normalizePhone($sFrom) : $this->normalizePhone($this->_sFromNumber));
+            $client->messages->create($this->normalizePhone($sTo), $aParams);
+            return true;
+        }
+        catch (Exception $oException) {
+            $this->writeLog($oException->getFile() . ':' . $oException->getLine() . ' ' . $oException->getMessage());
+            return false;
+        }
+    }
+    
+    public function normalizePhone($sPhone){
+		$sPhone = trim($sPhone);
+		if (substr($sPhone, 0, 1) != '+'){
+			$sPhone = '+' . $sPhone;	
+		}
+		return $sPhone;
+	}
+    
+    private function writeLog($sString)
+	{
+        bx_log('sys_twilio', $sString);
+	}
+}
+
+/** @} */

+ 453 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolUploader.php

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

+ 237 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/classes/BxDolVoteReactions.php

@@ -0,0 +1,237 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Like based votes for any content
+ */
+class BxDolVoteReactions extends BxTemplVote
+{
+    protected static $_sCounterStyleSimple = 'simple'; // total counter only.
+    protected static $_sCounterStyleDivided = 'divided'; // counters [icon + counter] divided by reactions.
+    protected static $_sCounterStyleCompound = 'compound'; // total counter with a list of reaction icons.
+
+    protected $_sMenuDoVote; //--- Do vote (reaction)  menu name.
+
+    protected $_sDataList; //--- Reactions list name.
+    protected $_aDataList; //--- Reactions list with all reactions.
+
+    protected $_sDefault; //--- Default reaction name.
+
+    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+
+        $this->_oQuery = new BxDolVoteReactionsQuery($this);
+        $this->_sType = BX_DOL_VOTE_TYPE_REACTIONS;
+
+        $this->_sMenuDoVote = 'sys_vote_reactions_do';
+
+        $this->_sDataList = 'sys_vote_reactions';
+        $this->_aDataList = array();
+
+        $this->_sDefault = 'default';
+    }
+
+    public function init($iId)
+    {
+        if(!parent::init($iId))
+            return false;
+
+        $aReactions = BxDolFormQuery::getDataItems($this->_sDataList, false, BX_DATA_VALUES_ALL);
+
+        foreach($aReactions as $sReaction => $aReaction) {
+            $aData = !empty($aReaction['Data']) ? unserialize($aReaction['Data']) : array();
+
+            $this->_aDataList[$sReaction] = array(
+                'name' => $sReaction,
+                'title' => $aReaction['LKey'],
+                'title_aux' => $aReaction['LKey2'],
+                'icon' => isset($aData['icon']) ? $aData['icon'] : '',
+                'color' => isset($aData['color']) ? $aData['color'] : '',
+                'weight' => isset($aData['weight']) ? $aData['weight'] : 1,
+            );
+        }
+
+        if(!empty($this->_aDataList)) {
+            $this->_aDataList[$this->_sDefault] = reset($this->_aDataList);
+            $this->_aDataList[$this->_sDefault]['color'] = '';
+        }
+    }
+    /**
+     * Interface functions for outer usage
+     */
+    public function getValue()
+    {
+        return (int)$this->_aSystem['min_value'];
+    }
+
+    public function getDefault()
+    {
+        return $this->_sDefault;
+    }
+
+    public function getReaction($sName)
+    {
+        if(empty($this->_aDataList))
+            $this->getReactions();
+
+        return !empty($this->_aDataList[$sName]) ? $this->_aDataList[$sName] : false;
+    }
+
+    public function getReactions($bFullInfo = false)
+    {
+        return $bFullInfo ? $this->_aDataList : array_keys($this->_aDataList);
+    }
+
+    public function getTrackBy($aParams)
+    {
+        return $this->_oQuery->getTrackBy($aParams);
+    }
+    
+    public function getIcon($sReaction, $bWithColor = true)
+    {
+        $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault];
+
+    	return $aReaction['icon'] . ($bWithColor && !empty($aReaction['color']) ? ' ' . $aReaction['color'] : '');
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionGetDoVotePopup()
+    {
+        if(!$this->isEnabled())
+           return '';
+
+        return $this->_getDoVotePopup((int)bx_get('value'));
+    }
+
+    public function actionGetVotedBy()
+    {
+        if(!$this->isEnabled())
+           return '';
+
+        $aParams = array();
+
+        $sReaction = bx_get('reaction');
+        if($sReaction !== false) {
+            $sReaction = bx_process_input($sReaction);
+            
+            $aReactions = $this->getReactions();
+            if(!in_array($sReaction, $aReactions))
+                $sReaction = $this->_sDefault;
+
+            $aParams['reaction'] = $sReaction;
+        }
+
+        return $this->_getVotedBy($aParams);
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _isDuplicate($iObjectId, $iAuthorId, $iAuthorIp, $bVoted)
+    {
+        return $bVoted && !$this->isUndo();
+    }
+
+    protected function _isCount($aVote = array())
+    {
+        if(empty($aVote))
+            $aVote = $this->_getVote();
+
+        foreach($aVote as $sKey => $mixedValue)
+            if(substr($sKey, 0, 5) == 'count' && (int)$mixedValue != 0)
+                return true;
+
+        return false;
+    }
+
+    protected function _getVoteData()
+    {
+        $aData = parent::_getVoteData();
+        if($aData === false)
+            return false;
+
+        $sReaction = bx_get('reaction');
+        if($sReaction === false)
+            return false;
+
+        $aData['reaction'] = bx_process_input($sReaction);
+        return $aData;
+    }
+
+    protected function _returnVoteData($iObjectId, $iAuthorId, $iAuthorIp, $aData, $bVoted, $aParams = array())
+    {
+        $aReactions = $this->getReactions();
+        $sReaction = $aData['reaction'];
+
+        $bUndo = $this->isUndo();
+        $bDisabled = $bVoted && !$bUndo;
+
+        $aVote = $this->_getVote($iObjectId, true);
+        $aTrack = $bVoted ? $this->_getTrack($iObjectId, $iAuthorId) : array();
+
+        $sJsClick = '';
+        if(!$bDisabled)
+            $sJsClick = $bVoted && $bUndo ? $this->getJsClickDo($sReaction) : $this->getJsClick();
+
+        $iTotalC = $iTotalS = 0;
+        foreach($aReactions as $sName) {
+            $iTotalC += (int)$aVote['count_' . $sName];
+            $iTotalS += (int)$aVote['sum_' . $sName];
+        }
+        $fTotalR = $iTotalC != 0 ? round($iTotalS / $iTotalC, 2) : 0;
+
+        $iCount = (int)$aVote['count_' . $sReaction];
+        $aResult = array(
+            'code' => 0,
+            'reaction' => $sReaction,
+            'rate' => $aVote['rate_' . $sReaction],
+            'count' => $iCount,
+            'countf' => $iCount > 0 ? $this->_getCounterLabel($iCount, array('reaction' => $sReaction)) : '',
+            'label_icon' => $this->_getIconDoWithTrack($bVoted, $aTrack),
+            'label_title' => _t($this->_getTitleDoWithTrack($bVoted, $aTrack)),
+            'label_click' => $sJsClick,
+            'disabled' => $bVoted && !$this->isUndo(),
+            'total' => array(
+                'rate' => $fTotalR,
+                'count' => $iTotalC,
+                'countf' => $iTotalC > 0 ? $this->_getCounterLabel($iTotalC, array('show_counter_label_icon' => false, 'reaction' => '')) : '',
+            )
+        );
+
+        return $aResult;
+    }
+
+    protected function _getIconDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->getIcon($sReaction);
+    }
+
+    protected function _getTitleDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->_aDataList[$sReaction]['title'];
+    }
+    
+    protected function _getTitleDoBy($aParams = array())
+    {
+        if(isset($aParams['show_counter_style']) && $aParams['show_counter_style'] == self::$_sCounterStyleCompound)
+            return _t('_vote_do_by_reactions');
+
+        $sReaction = !empty($aParams['reaction']) ? $aParams['reaction'] : $this->_sDefault;
+    	return _t('_vote_do_by_x_reaction', _t($this->_aDataList[$sReaction]['title']));
+    }
+}
+
+/** @} */

+ 178 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/design.inc.php

@@ -0,0 +1,178 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * design box with content only - no borders, no background, no caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_CONTENT_ONLY', 0);
+
+/**
+ * default design box with content, borders and caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_DEF', 1);
+
+/**
+ * just empty design box, without anything
+ * @see DesignBoxContent
+ */
+define('BX_DB_EMPTY', 2);
+
+/**
+ * design box with content, like BX_DB_DEF but without caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_NO_CAPTION', 3);
+
+/**
+ * design box with content only wrapped with default padding - no borders, no background, no caption
+ * it can be used to just wrap content with default padding
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_CONTENT_ONLY', 10);
+
+/**
+ * default design box with content wrapped with default padding, borders and caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_DEF', 11);
+
+/**
+ * design box with content wrapped with default padding, like BX_DB_DEF but without caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_NO_CAPTION', 13);
+
+/*
+ * Menu template IDs for block submenu
+ */
+define('BX_DB_MENU_TEMPLATE_TABS', 25);
+define('BX_DB_MENU_TEMPLATE_POPUP', 26);
+
+define('BX_DB_HIDDEN_PHONE', 1);
+define('BX_DB_HIDDEN_TABLET', 2);
+define('BX_DB_HIDDEN_DESKTOP', 3);
+define('BX_DB_HIDDEN_MOBILE', 4);
+
+define('BX_FORMAT_DATE', 'sys_format_date'); ///< date format identifier for use in @see bx_time_js function
+define('BX_FORMAT_TIME', 'sys_format_time'); ///< time format identifier for use in @see bx_time_js function
+define('BX_FORMAT_DATE_TIME', 'sys_format_datetime'); ///< datetime format identifier for use in @see bx_time_js function
+
+/*
+ * Menu template IDs for page submenus
+ */
+define('BX_MENU_TEMPLATE_SUBMENU', 8);
+define('BX_MENU_TEMPLATE_SUBMENU_MORE_AUTO', 18);
+
+/*
+ * Menu template IDs for Custom menu
+ */
+define('BX_MENU_TEMPLATE_CUSTOM_HOR', 15);
+define('BX_MENU_TEMPLATE_CUSTOM_VER', 20);
+
+/**
+ * Display "design box" HTML code
+ *
+ * @see BxBaseFunctions::DesignBoxContent
+ *
+ * @see BX_DB_CONTENT_ONLY
+ * @see BX_DB_DEF
+ * @see BX_DB_EMPTY
+ * @see BX_DB_NO_CAPTION
+ * @see BX_DB_PADDING_CONTENT_ONLY
+ * @see BX_DB_PADDING_DEF
+ * @see BX_DB_PADDING_NO_CAPTION
+ */
+function DesignBoxContent ($sTitle, $sContent, $iTemplateNum = BX_DB_DEF, $mixedMenu = false)
+{
+    return BxTemplFunctions::getInstance()->designBoxContent ($sTitle, $sContent, $iTemplateNum, $mixedMenu);
+}
+
+/**
+ * Use this function in pages if you want to not cache it.
+ **/
+function send_headers_page_changed()
+{
+    $now = gmdate('D, d M Y H:i:s') . ' GMT';
+
+    header("Expires: $now");
+    header("Last-Modified: $now");
+    header("Cache-Control: no-cache, must-revalidate");
+    header("Pragma: no-cache");
+}
+
+function MsgBox($sText, $iTimer = 0)
+{
+    return BxTemplFunctions::getInstance()->msgBox($sText, $iTimer);
+}
+
+function PopupBox($sName, $sTitle, $sContent, $isHiddenByDefault = false)
+{
+    return BxTemplFunctions::getInstance()->popupBox($sName, $sTitle, $sContent, $isHiddenByDefault);
+}
+
+function getVersionComment()
+{
+    $aVer = explode('.', bx_get_ver());
+
+    // version output made for debug possibilities.
+    // randomizing made for security issues. do not change it...
+    $aVerR[0] = $aVer[0];
+    $aVerR[1] = rand( 0, 100 );
+    $aVerR[2] = $aVer[1];
+    $aVerR[3] = rand( 0, 100 );
+    $aVerR[4] = $aVer[2];
+
+    return '<!-- ' . implode(' ', $aVerR) . ' -->';
+}
+
+function getSiteStatUser()
+{
+    $sqlQuery = "SELECT * FROM `sys_statistics` ORDER BY `order`";
+
+    $aStat = BxDolDb::getInstance()->fromCache('sys_statistics', 'getAllWithKey', $sqlQuery, 'name');
+
+    return "<pre>TODO: nice output\n" . print_r($aStat, true) . '</pre>';
+}
+
+/**
+ * Output time wrapped in &lt;time&gt; tag in HTML.
+ * Then time is autoformatted using JS upon page load, this is aumatically converted to user's timezone and
+ * updated in realtime in case of short periods of 'from now' time format.
+ *
+ * This is just short version for:
+ * @see BxTemplFunctions::timeForJs
+ *
+ * @param $iUnixTimestamp time as unixtimestamp
+ * @param $sFormatIdentifier output format identifier
+ *     @see BX_FORMAT_DATE
+ *     @see BX_FORMAT_TIME
+ *     @see BX_FORMAT_DATE_TIME
+ * @param $bForceFormat force provided format and don't use "from now" time autoformat.
+ */
+function bx_time_js ($iUnixTimestamp, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false)
+{
+    return BxTemplFunctions::getInstance()->timeForJs ($iUnixTimestamp, $sFormatIdentifier, $bForceFormat);
+}
+
+/**
+ * Get UTC/GMT time string in ISO8601 date format from provided unix timestamp
+ * @param $iUnixTimestamp - unix timestamp
+ * @return ISO8601 formatted date/time string
+ */
+function bx_time_utc ($iUnixTimestamp)
+{
+    return gmdate(DATE_ISO8601, (int)$iUnixTimestamp);
+}
+
+$oZ = new BxDolAlerts('system', 'design_included', 0);
+$oZ->alert();
+
+/** @} */

+ 47 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCategories.js

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ *
+ * @{
+ */
+
+function BxDolCategories(oOptions) {
+    this._sActionsUrl = oOptions.sActionUrl;
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxDolCategories' : oOptions.sObjName;
+
+    this._sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'slide' : oOptions.sAnimationEffect;
+    this._iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+    this._oRequestParams = oOptions.oRequestParams == undefined ? {} : oOptions.oRequestParams;
+}
+
+BxDolCategories.prototype.categoryAdd = function (oButton, sName) {
+    var oButton = $(oButton);
+
+    var oCategory = oButton.parents('#bx-form-element-' + sName).find('.bx-form-input-category:first').clone();
+    oCategory.find("input[type = 'text']").val('');
+    oCategory.find("input[type = 'hidden']").remove();
+
+    oButton.parents('.bx-form-input-category-add:first').before(oCategory);
+};
+
+BxDolCategories.prototype.categoryAddNew = function (oButton, sName) {
+    var oButton = $(oButton);
+
+    var oCategory = oButton.parents('#bx-form-element-' + sName).find('.bx-form-input-category-new:first').clone();
+    oCategory.find("input[type = 'text']").val('');
+    oCategory.find("input[type = 'hidden']").remove();
+
+    $('.bx-form-input-categories .bx-form-input-category-add:first').before(oCategory);
+};
+
+BxDolCategories.prototype.categoryDelete = function (oButton) {
+    $(oButton).parents('.bx-form-input-category:first').remove();
+};
+
+BxDolCategories.prototype.categoryDeleteNew = function (oButton) {
+    $(oButton).parents('.bx-form-input-category-new:first').remove();
+};

+ 814 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCmts.js

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

+ 107 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolCreatePost.js

@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolCreatePost(oOptions) {
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxDolCreatePost' : oOptions.sObjName;
+    this._sRootUrl = oOptions.sRootUrl == undefined ? sUrlRoot : oOptions.sRootUrl;
+    this._sDefault = oOptions.sDefault == undefined ? '' : oOptions.sDefault;
+    this._iContextId = oOptions.iContextId == undefined ? 0 : oOptions.iContextId;
+    this._oCustom = oOptions.oCustom == undefined ? {} : oOptions.oCustom;
+
+    this._sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this._iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+}
+
+BxDolCreatePost.prototype.getForm = function (sModuleName, sModuleUri, oElement) {
+    var $this = this;
+    var oDate = new Date();
+
+    var oTab = $(oElement);
+    var sTabActive = 'bx-menu-tab-active';
+    oTab.parents('.bx-db-menu:first').find('li.' + sTabActive).removeClass(sTabActive);
+    oTab.parents('li:first').addClass(sTabActive);
+    oTab.parents('.bx-popup-applied:visible').dolPopupHide();
+
+    if($('.sys-cpf-form.sys-cpf-' + sModuleName + ' form').length > 0) {
+        $('.sys-cpf-form:visible').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+            $('.sys-cpf-form.sys-cpf-' + sModuleName).show().siblings('.sys-cpf-close:hidden').show();
+        });
+
+        return false;
+    }
+
+    this._loadingInBlock(oElement, true);
+
+    $.get(
+        this._sRootUrl + 'modules/?r=' + sModuleUri + '/get_create_post_form/', {
+            ajax_mode: true,
+            dynamic_mode: true,
+            absolute_action_url: true,
+            context_id: this._iContextId,
+            custom: this._oCustom,
+            _t:oDate.getTime()
+        },
+        function(oData) {
+            $this._loadingInBlock(oElement, false);
+
+            if(!oData || !oData.content) 
+                return;
+
+            $('.sys-cpf-form:visible').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+                $this._loadingInBlock(oElement, true);
+
+                $('.sys-cpf-close:visible').hide();
+
+                var sForm = '.sys-cpf-form.sys-cpf-' + oData.module;
+                if($(sForm).length == 0)
+                    $('.sys-cpf-close').before($('<div class="sys-cpf-form sys-cpf-' + oData.module + '"></div>').hide());
+
+                $(sForm).html(oData.content).show().siblings('.sys-cpf-close:hidden').show();
+
+                $this._loadingInBlock(oElement, false);
+            });
+        }, 
+        'json'
+    );
+
+    return false;
+};
+
+BxDolCreatePost.prototype.hideForm = function(oElement) {
+    $(oElement).parents('.sys-cpf-close:first').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed);
+
+    var sTabActive = 'bx-menu-tab-active';
+    var sTabDefault = 'bx-menu-item-' + this._sDefault;
+    
+    var oMiActive = $('.sys-cpf-submenu li.' + sTabActive);
+    var bMiChange = oMiActive.not('.' + sTabDefault);
+
+    if(bMiChange)
+        oMiActive.removeClass(sTabActive);
+
+    $('.sys-cpf-form:visible').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+        $('.sys-cpf-form.sys-cpf-default').show();
+
+        if(bMiChange)
+            $('.' + sTabDefault).addClass(sTabActive);
+    });
+};
+
+BxDolCreatePost.prototype._loading = function(e, bShow) {
+    var oParent = $(e).length ? $(e) : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolCreatePost.prototype._loadingInBlock = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('.bx-db-content:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+/** @} */

+ 206 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolFavorite.js

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

+ 69 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolForm.js

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolForm(oOptions)
+{
+    if(typeof oOptions === 'undefined')
+        return;
+
+    this._sObjName = oOptions.sObjName === undefined ? 'oForm' : oOptions.sObjName; // javascript object name, to run current object instance from onTimer
+    this._sObject = oOptions.sObject; // form object
+    this._sDisplay = oOptions.sDisplay; // form display
+
+    this._sActionsUri = 'form.php';
+    this._sActionsUrl = oOptions.sRootUrl + this._sActionsUri; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+    this._aHtmlIds = oOptions.aHtmlIds;
+}
+
+BxDolForm.prototype.pgcTogglePopup = function(oLink, iInputId, sPrivacyObject)
+{
+    var oData = this._getDefaultParams();
+    oData['a'] = 'get_privacy_group_chooser';
+    oData['input_id'] = iInputId;
+    oData['privacy_object'] = sPrivacyObject;
+
+    $(oLink).dolPopupAjax({
+        id: this._aHtmlIds['pgc_popup'] + iInputId, 
+        url: bx_append_url_params(this._sActionsUri, oData),
+        closeOnOuterClick: false,
+        removeOnClose: true,
+    });
+};
+
+BxDolForm.prototype.pgcOnSelectGroup = function(oData)
+{
+    if(oData && parseInt(oData.code) != 0)
+        return;
+
+    if(oData.form_id && oData.chooser_id && oData.icon)
+        $('#' + oData.form_id + ' #' + oData.chooser_id + ' .bx-form-input-pgc-current .sys-icon').removeClass().addClass('sys-icon ' + oData.icon);
+};
+
+BxDolForm.prototype._getDefaultParams = function() 
+{
+    var oDate = new Date();
+    return {
+        o: this._sObject,
+        d: this._sDisplay,
+        _t: oDate.getTime()
+    };
+};
+
+BxDolForm.setCheckBoxValue = function (obj) {
+    var oHidden = $(obj).parent('div').find('INPUT[type=hidden]')
+    var val = 0;
+    if ($(obj).attr("checked") == 'checked') {
+        val = 1;
+    }
+    oHidden.val(val);
+}
+
+/** @} */

+ 476 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolGrid.js

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

+ 97 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolLabel.js

@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolLabel(oOptions) {
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxDolLabel' : oOptions.sObjName;
+
+    this._sRootUrl = oOptions.sRootUrl == undefined ? sUrlRoot : oOptions.sRootUrl;
+    this._sActionsUrl = this._sRootUrl + 'label.php'; // actions url address
+
+    this._sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this._iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+
+    this._sClassItem = 'sys-labels-list-item';
+    this._sClassSublist = 'sys-labels-li-sublist';
+}
+
+BxDolLabel.prototype.selectLabels = function(oElement, sName, aParams)
+{
+    var $this = this;
+
+    var aValue = new Array();
+    $(oElement).find('input:hidden').each(function() {
+        aValue.push($(this).val());
+    });
+
+    var oData = $.extend({}, $this._getDefaultParams(), {action: 'select_labels', name: sName, value: aValue}, (aParams || {}));
+
+    this._loadingInFormElement(oElement, true);
+
+    $.get(
+        this._sActionsUrl, 
+        oData,
+        function(oData) {
+            $this._loadingInFormElement(oElement, false);
+
+            processJsonData(oData);
+        }, 
+        'json'
+    );
+};
+
+BxDolLabel.prototype.onSelectLabels = function(oData)
+{
+    if(!oData.name || !oData.content)
+        return;
+
+    var oElement = $('#' + this._aHtmlIds['labels_element'] + oData.name + ' .bx-form-input-labels');
+    if(!oElement.length)
+        return;
+
+    oElement.find('b.val:not(.val-placeholder)').remove();
+    oElement.prepend(oData.content);
+};
+
+BxDolLabel.prototype.showSublist = function(oLink)
+{
+    $(oLink).find('.sys-icon').toggleClass('chevron-down').toggleClass('chevron-up').parents('.' + this._sClassItem + ':first').find('.' + this._sClassSublist + ':first').bx_anim('toggle', this._sAnimationEffect, this._iAnimationSpeed);
+};
+
+BxDolLabel.prototype._loading = function(e, bShow)
+{
+    var oParent = $(e).length ? $(e) : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolLabel.prototype._loadingInBlock = function(e, bShow)
+{
+    var oParent = $(e).length ? $(e).parents('.bx-db-content:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolLabel.prototype._loadingInFormElement = function(e, bShow)
+{
+    var oParent = $(e).length ? $(e).parents('.bx-form-element:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolLabel.prototype._getSelectLabelsElement = function()
+{
+    return $('#' + this._aHtmlIds['form'] + ' #' + this._aHtmlIds['labels_element'] + ' input');
+};
+
+BxDolLabel.prototype._getDefaultParams = function() 
+{
+    var oDate = new Date();
+    return {
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 101 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolLiveUpdates.js

@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolLiveUpdates(oOptions)
+{
+    this._sActionsUrl = oOptions.sActionsUrl == undefined ? sUrlRoot + 'live_updates.php' : oOptions.sActionsUrl;
+    this._sObjName = oOptions.sObjName == undefined ? 'oLiveUpdates' : oOptions.sObjName;
+    this._iInterval = oOptions.iInterval == undefined ? 3000 : oOptions.iInterval;
+    this._aSystemsActive = oOptions.aSystemsActive == undefined ? {} : oOptions.aSystemsActive;
+    this._aSystemsTransient = oOptions.aSystemsTransient == undefined ? {} : oOptions.aSystemsTransient;
+    this._bServerRequesting = oOptions.bServerRequesting == undefined ? false : oOptions.bServerRequesting;
+    this._sHash = oOptions.sHash == undefined ? '' : oOptions.sHash;
+
+    this._iIndex = 0;
+    this._iHandler = 0;
+    this._bBusy = false;
+
+    this.init();
+}
+
+BxDolLiveUpdates.prototype.init = function() {
+    var $this = this;
+
+    if(this._iHandler)
+        this.destroy();
+
+    $(document).ready(function() {
+        $this.perform(); // make first call straight away to load addons for menus
+
+    	$this._iHandler = setInterval(function() {
+            $this.perform();
+    	}, $this._iInterval);
+    });
+};
+
+BxDolLiveUpdates.prototype.add = function(oData) {
+	if(!oData)
+		return;
+
+	if(oData.name != undefined && oData.value != undefined) {
+		if(!this._aSystemsActive[oData.name])
+			this._aSystemsActive[oData.name] = oData.value;
+	
+		if(!this._aSystemsTransient[oData.name])
+			this._aSystemsTransient[oData.name] = 1;
+	}
+
+	if(oData.hash != undefined)
+		this._sHash = oData.hash;
+};
+
+BxDolLiveUpdates.prototype.destroy = function() {
+	if(this._iHandler)
+		clearInterval(this._iHandler);
+};
+
+BxDolLiveUpdates.prototype.perform = function() {
+	if(!this._bServerRequesting || this._bBusy)
+		return;
+
+	var $this = this;
+	var oDate = new Date();
+
+	this._bBusy = true;
+
+    $.post(
+    	this._sActionsUrl,
+        {
+    		index: this._iIndex,
+    		systems_active: this._aSystemsActive,
+    		systems_transient: this._aSystemsTransient,
+    		hash: this._sHash,
+    		_t: oDate.getTime()
+        },
+        function(aData) {
+        	$.each(aData, function(iIndex, oValue) {
+        		if(oValue.system && $this._aSystemsActive[oValue.system] != undefined && oValue.data)
+        			$this._aSystemsActive[oValue.system] = oValue.data.count_new;
+
+        		if(oValue.method) {
+        			var oFunc = function(oData) {
+        				eval(oValue.method);
+        			};
+        			oFunc(oValue.data);
+        		}
+        	});
+
+        	$this._bBusy = false;
+        },
+        'json'
+    );
+
+    this._iIndex += 1;
+};
+
+/** @} */

+ 176 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolMenuMoreAuto.js

@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolMenuMoreAuto(options)
+{
+    this._sObject = options.sObject;
+    this._iItemsStatic = undefined == options.iItemsStatic ? 0 : options.iItemsStatic;
+    this._bItemsStaticOnly = undefined == options.bItemsStaticOnly ? 0 : options.bItemsStaticOnly;
+    this._aHtmlIds = undefined == options.aHtmlIds ? {} : options.aHtmlIds;  
+
+    this._sKeyWidth = 'bx-mma-width';
+
+    this._sClassReady = 'bx-mma-ready';
+    this._sClassItem = '.bx-menu-item';
+    this._sClassItemMore = this._sClassItem + '.bx-menu-item-more-auto';
+    this._sClassItemMoreSubmenu = '.bx-menu-submenu-more-auto';
+    this._sClassItemStatic = 'bx-menu-item-static';
+
+    this._bStaticMode = this._bItemsStaticOnly && this._iItemsStatic > 0;
+    this._bTrackContainerResize = false;
+}
+
+BxDolMenuMoreAuto.prototype.init = function() {
+    var $this = this;
+
+    var oMenu = $('#' + this._aHtmlIds['main']);
+    if(!oMenu.length) 
+        return;
+
+    //--- Initialize ---//
+    var oItemMore = oMenu.find(this._sClassItemMore);
+    var oItemMoreSubmenu = oItemMore.find(this._sClassItemMoreSubmenu);
+
+    for(var i = 0; i < this._iItemsStatic; i++)
+        oItemMore.prevAll(this._sClassItem + ':not(.' + this._sClassItemStatic + '):last').addClass(this._sClassItemStatic);
+
+    var iMenu = 0;
+    oMenu.children(this._sClassItem + ':visible').each(function() {
+        iMenu += $this._getWidth($(this));
+    });
+
+    var iParent = oMenu.parent().width();
+    var iItemMore = oItemMore.outerWidth(true);
+
+    oMenu.css('overflow', 'visible');
+
+    if(iMenu >= iParent || this._bStaticMode)
+        this._moveToSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore);
+
+    //--- Add event handlers ---//
+    if(!this._bStaticMode) {
+        $(window).on('resize', function() {
+           $this.update();
+        });
+
+        if(this._bTrackContainerResize)
+            setTimeout(function () {
+                oMenu.find(this._sClassItem).on('resize', function() {
+                    $this.update(true);
+                });
+            }, 200);
+    }
+};
+
+BxDolMenuMoreAuto.prototype.update = function(bForceCalculate)
+{
+    if(this._bStaticMode)
+        return;
+
+    var $this = this;
+    var oMenu = $('#' + this._aHtmlIds['main']);
+    var oItemMore = oMenu.find(this._sClassItemMore);
+    var oItemMoreSubmenu = oItemMore.find(this._sClassItemMoreSubmenu);
+
+    var iMenu = 0;
+    oMenu.children(this._sClassItem + ':visible').each(function() {
+        iMenu += $this._getWidth($(this), bForceCalculate);
+    });
+
+    var iParent = oMenu.parent().width();
+    var iItemMore = oItemMore.outerWidth(true);
+
+    if(iMenu > iParent)
+        this._moveToSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore);
+    if(iMenu < iParent)
+        this._moveFromSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iMenu);
+};
+
+BxDolMenuMoreAuto.prototype.more = function(oElement)
+{
+    var oElement = $(oElement);
+
+    oElement.parents('li:first').find('#' + this._aHtmlIds['more_auto_popup']).dolPopup({
+        pointer: {
+            el: oElement
+        }, 
+        moveToDocRoot: false
+    });
+}
+
+BxDolMenuMoreAuto.prototype._moveToSubmenu = function(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore)
+{
+    var $this = this;
+
+    var bRelocateOthers = false;
+    var iWidthTotal = iItemMore;
+    var oSubmenuItemFirst = oItemMoreSubmenu.children(this._sClassItem + ':first');
+
+    oMenu.children(this._sClassItem + ':not(' + this._sClassItemMore + ')').each(function() {
+        var oItem = $(this);
+        var iItem = $this._getWidth(oItem);
+        if(($this._bStaticMode || bRelocateOthers || iWidthTotal + iItem > iParent) && !oItem.hasClass($this._sClassItemStatic)) {
+            oItem.addClass('bx-def-color-bg-hl-hover');
+
+            if(!oSubmenuItemFirst.length)
+                oItemMoreSubmenu.append(oItem.detach());
+            else
+                oSubmenuItemFirst.before(oItem.detach());
+            bRelocateOthers = true;
+            return;
+        }
+
+        iWidthTotal += iItem;
+    });
+
+    if(oItemMoreSubmenu.find('li').length)
+        oItemMore.show(function() {
+            oMenu.addClass($this._sClassReady).parents('.bx-menu-more-auto-wrapper:first').css('overflow', 'visible');
+        });
+};
+
+BxDolMenuMoreAuto.prototype._moveFromSubmenu = function(oMenu, oItemMore, oItemMoreSubmenu, iParent, iMenu)
+{
+    var $this = this;
+
+    var bStopRelocation = false;
+    var iWidthTotal = iMenu;
+    oItemMoreSubmenu.children(this._sClassItem).each(function() {
+        if(bStopRelocation) 
+            return;
+
+        var oItem = $(this);
+        var iItem = $this._getWidth(oItem);
+        if(iWidthTotal + iItem > iParent) {
+            bStopRelocation = true;
+            return;
+        }
+
+        oItem.removeClass('bx-def-color-bg-hl-hover');
+
+        oItemMore.before(oItem.detach());
+        iWidthTotal += iItem;
+    });
+
+    if(!oItemMoreSubmenu.find('li').length)
+        oItemMore.hide();
+};
+
+BxDolMenuMoreAuto.prototype._getWidth = function(oItem, bForceCalculate)
+{
+    var iItem = parseInt(oItem.attr(this._sKeyWidth));
+    if(!bForceCalculate && iItem)
+        return iItem;
+
+    iItem = oItem.outerWidth(true);
+    if(iItem)
+        oItem.attr(this._sKeyWidth, iItem);
+
+    return iItem;
+}
+/** @} */

+ 103 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolNestedForm.js

@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolNestedForm(options) {
+
+    this.init(options);
+    this._eForm = null;
+};
+
+
+BxDolNestedForm.prototype.init = function (options) {
+	this._sUniqId = options.uniq_id;
+    this._sJsInstanceName = options.js_instance_name;
+    this._sResultContainerId = 'bx-form-input-nested-form-forms-' + this._sUniqId;
+	this._sNestedType = options.nested_type ? options.nested_type : '';
+   	this._sTemplateGhost = (options.template_ghost && typeof options.template_ghost == "string") ? options.template_ghost.replace(/{nested_type}/gi, options.nested_type) : '';
+    this._sTemplateErrorGhosts = options.template_error ? options.template_error : '<div class="bx-informer-msg-error bx-def-padding-sec bx-def-round-corners bx-def-margin-sec-top">{error}</div>';
+    this._sFormName = options.form_name ? options.form_name : '';
+    this._sActionsUri = options.action_uri ? options.action_uri : '';
+	this._iContentId = undefined == options.content_id || '' == options.content_id ? '' : parseInt(options.content_id);	
+};
+
+BxDolNestedForm.prototype._getUrlWithStandardParams = function () {
+    return sUrlRoot + this._sActionsUri + 'Nested?uid=' + this._sUniqId + '&s=' + this._sFormName + '&t=' + this._sNestedType
+};
+
+BxDolNestedForm.prototype._getContainerId = function (iItemId) {
+	return 'bx-nested-form-' + this._sNestedType + '-' + iItemId;
+};
+
+BxDolNestedForm.prototype.addEmptyGhost = function () {
+    var oVars = {
+        js_instance_name: this._sJsInstanceName,
+        file_id: 0,
+        item_id: 0,
+    }
+    var sHTML = this._sTemplateGhost;
+    for (var i in oVars)
+        sHTML = sHTML.replace (new RegExp('{'+i+'}', 'g'), oVars[i]);
+	
+	$.each(this._aFields, function( index, value ) {
+		sHTML = sHTML.replace (new RegExp('{'+value+'}', 'g'), '');
+	});
+
+    $('#' + this._sResultContainerId).append(sHTML).addWebForms();
+};
+
+BxDolNestedForm.prototype.deleteGhost = function (e, sSelector) {
+
+	iItemId = 0;
+	$(e).closest('.bx-nested-form-ghost-form').each(function() {
+  		iItemId = $( this ).find("input[name='"+sSelector+"[]']").val()
+	});
+	
+	if (0 == parseInt(iItemId) || typeof(iItemId) == "undefined") {
+        $(e).closest('.bx-nested-form-ghost').slideUp(function () {
+            $(this).remove();
+        });
+    }
+    else {
+
+        var sUrl = this._getUrlWithStandardParams() + '&a=delete&id=' + iItemId;
+        var $this = this;
+
+        var sContainerId = $this._getContainerId(iItemId);
+        bx_loading(sContainerId, true);
+		
+		$.post(sUrl, function (sMsg) {
+            bx_loading(sContainerId, false);
+            if ('ok' == sMsg) {
+                $('#' + sContainerId).slideUp('slow', function () {
+                    $(this).remove();
+                });
+            } else {
+                $('#' + $this._sResultContainerId).prepend($this._sTemplateErrorGhosts.replace('{error}', sMsg));
+            }
+        });
+    }
+};
+
+BxDolNestedForm.prototype._replaceVars = function (sHTML, oVars) {
+
+    oVars = $.extend({}, oVars, {js_instance_name: this._sJsInstanceName});
+	
+    // replace vars
+    for (var i in oVars) {
+        sHTML = sHTML.replace (new RegExp('{'+i+'}', 'g'), oVars[i]);
+	}
+
+    // set selected values
+    var oHTML = $(sHTML);
+    for (var i in oVars)
+        oHTML.find('select[name="' + i + '[]"] option[value="' + oVars[i] + '"]').attr('selected', 'selected');
+    sHTML = $('<div></div>').append(oHTML).html();
+
+    return sHTML;
+};
+/** @} */

+ 59 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolPage.js

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+function BxDolPage(oOptions)
+{
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxDolPage' : oOptions.sObjName;
+    this._isStickyColumns = oOptions.isStickyColumns == undefined ? false : oOptions.isStickyColumns;
+    this._iLastSc = 0;
+    var $this = this;
+    $(document).ready(function () {
+        $this.init();
+    });
+}
+
+BxDolPage.prototype.init = function () {
+    var $this = this;
+    if ($this._isStickyColumns && !$('html').hasClass('bx-media-phone')) {
+        $(window).resize(function () { $this.stickyBlocks() });
+        $(window).scroll(function () { $this.stickyBlocks() });
+    }
+};
+
+BxDolPage.prototype.stickyBlocks = function () {
+    iSc = $(window).scrollTop();
+    $.each($('.bx-layout-col'), function (index, val) {
+        if ($(this).css('position') == 'sticky') {
+            var iCh = $(this).height();
+            var iWh = $(window).height();
+            if (iCh > iWh) {
+                if (iCh - $(window).scrollTop() - $(window).height() < 0) {
+                    var iMinS = (iWh - iCh);
+                    if ($(this).css('top') == '') {
+                        $(this).css('top', iMinS + 'px');
+                    }
+                    else {
+                        var iCurS = new Number($(this).css('top').replace('px', '')) - iSc + this._iLastSc;
+                        if (iCurS > 0) {
+                            $(this).css('top', '0px');
+                        }
+                        if (iCurS < iMinS) {
+                            $(this).css('top', iMinS + 'px');
+                        }
+                        if (iCurS < 0 && iCurS > iMinS) {
+                            $(this).css('top', iCurS + 'px');
+                        }
+                    }
+                }
+            }
+        }
+    });
+    this._iLastSc = iSc <= 0 ? 0 : iSc;
+}
+
+
+/** @} */

+ 649 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolUploader.js

@@ -0,0 +1,649 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Simple Uploader js class
+ */
+function BxDolUploaderSimple (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe';
+    this._eForm = null;
+}
+
+BxDolUploaderSimple.prototype.init = function (sUploaderObject, sStorageObject, sUniqId, options) {
+    this._isUploadsInProgress = false;
+
+    this._sUploaderObject = sUploaderObject;
+    this._sStorageObject = sStorageObject;
+
+    this._sUniqId = sUniqId;
+
+    this._sUploaderJsInstance = 'glUploader_' + sUniqId + '_' + this._sUploaderObject;
+    this._sUploadInProgressContainerId = 'bx-form-input-files-' + sUniqId + '-upload-in-progress-' + this._sUploaderObject;
+    this._sPopupContainerId = 'bx-form-input-files-' + sUniqId + '-popup-wrapper-' + this._sUploaderObject;
+
+    this._sResultContainerId = 'bx-form-input-files-' + sUniqId + '-upload-result';
+    this._sErrorsContainerId = 'bx-form-input-files-' + sUniqId + '-errors'; 
+
+    this._sFormContainerId = 'bx-form-input-files-' + sUniqId + '-form-cont';
+
+    this._sTemplateGhost = options.template_ghost ? options.template_ghost : '<div id="' + this._getFileContainerId('{file_id}') + '"><input type="hidden" name="f[]" value="{file_id}" />{file_name} (<a href="javascript:void(0);" onclick="{js_instance_name}.deleteGhost(\'{file_id}\')">delete</a>)</div>';
+    this._sTemplateError = options.template_error_msg ? options.template_error_msg : '<div>{error}</div>' ;
+    this._sTemplateErrorGhosts = options.template_error_ghosts ? options.template_error_ghosts : this._sTemplateError;
+
+    this._isMultiple = undefined == options.multiple || !options.multiple ? false : true;
+
+    this._iContentId = undefined == options.content_id || '' == options.content_id ? '' : parseInt(options.content_id);
+
+    this._sImagesTranscoder = options.images_transcoder ? options.images_transcoder : '';
+
+    this._isPrivate = undefined == options.storage_private || parseInt(options.storage_private) ? 1 : 0;
+
+    this._isErrorShown = false;
+}
+
+BxDolUploaderSimple.prototype.isMultiple = function () {    
+    return this._isMultiple;
+}
+
+BxDolUploaderSimple.prototype.getCurrentFilesCount = function () {
+    return $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').length;
+}
+
+BxDolUploaderSimple.prototype.showUploaderForm = function () {
+    var $this = this;
+    var sUrl = this._getUrlWithStandardParams() + '&a=show_uploader_form&m=' + (this._isMultiple ? 1 : 0) + '&c=' + this._iContentId + '&p=' + this._isPrivate + '&_t=' + escape(new Date());
+
+    $(window).dolPopupAjax({
+        url: sUrl,
+        id: {force: true, value: this._sPopupContainerId},
+        onBeforeShow: function() {
+            if ($this.isMultiple())
+                $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').show();
+            else
+                $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').hide();
+            $('#' + $this._sPopupContainerId + ' .bx-popup-element-close').click(function() {
+                $this.onClickCancel();
+            });
+            if ('undefined' !== typeof($this.onBeforeShowPopup))
+                $this.onBeforeShowPopup();
+        },
+        onShow: function () {
+            if ('undefined' !== typeof($this.onShowPopup))
+                $this.onShowPopup();
+        },
+        closeElement: false,
+        closeOnOuterClick: false
+    });
+}
+
+BxDolUploaderSimple.prototype.onClickCancel = function () {
+	var $this = this;
+    if (this._isUploadsInProgress) {
+    	bx_confirm(_t('_sys_uploader_confirm_close_popup'), function() {
+    		$this.cancelAll();
+            $('#' + $this._sPopupContainerId).dolPopupHide({});
+    	});
+    } else {
+        $('#' + this._sPopupContainerId).dolPopupHide();
+    }
+}
+
+BxDolUploaderSimple.prototype.onBeforeUpload = function (params) {
+
+    this._eForm = params;
+
+    this._loading(true, false);
+        
+    this._isUploadsInProgress = true;
+    this._lockPageFromLeaving();
+    this._clearErrors();
+}
+
+BxDolUploaderSimple.prototype.onProgress = function (params) {
+
+}
+
+BxDolUploaderSimple.prototype.onUploadCompleted = function (sErrorMsg) {
+
+    this._isUploadsInProgress = false;
+    this._unlockPageFromLeaving();
+    this._loading(false, true);
+
+    if (sErrorMsg.length) {
+        this.restoreGhosts();        
+        this._showError(sErrorMsg);
+    } else {        
+        this.restoreGhosts();
+        $('#' + this._sPopupContainerId).dolPopupHide({});
+    }
+}
+
+BxDolUploaderSimple.prototype.cancelAll = function () {
+    $('#' + this._sIframeId).attr('src', 'javascript:false;');
+    this.onUploadCompleted(_t('_sys_uploader_upload_canceled'));
+}
+
+BxDolUploaderSimple.prototype.restoreGhosts = function () {
+    var sUrl = this._getUrlWithStandardParams() + '&img_trans=' + this._sImagesTranscoder + '&a=restore_ghosts&f=json' + '&c=' + this._iContentId + '&_t=' + escape(new Date());
+    var $this = this;
+
+    bx_loading(this._sResultContainerId, true);
+
+    $.getJSON(sUrl, function (aData) {
+
+        bx_loading($this._sResultContainerId, false);
+
+        if (!$this.isMultiple())
+            $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').remove();
+
+        if ('object' === typeof(aData)) {
+            $.each(aData, function(iFileId, oVars) {
+            	var oFileContainer = $('#' + $this._getFileContainerId(iFileId));
+                if (oFileContainer.length > 0)
+                    return;
+                var sHTML;
+                if (typeof $this._sTemplateGhost == 'object')
+                    sHTML = $this._sTemplateGhost[iFileId];
+                else
+                    sHTML = $this._sTemplateGhost;
+                for (var i in oVars)
+                    sHTML = sHTML.replace (new RegExp('{'+i+'}', 'g'), oVars[i]);
+                
+                $('#' + $this._sResultContainerId).prepend(sHTML);
+
+                oFileContainer.find('.bx-uploader-ghost-preview img').hide().fadeIn(1000);
+            });
+        }
+    });
+};
+
+BxDolUploaderSimple.prototype.deleteGhost = function (iFileId) {
+    var sUrl = this._getUrlWithStandardParams() + '&a=delete&id=' + iFileId;
+    var $this = this;
+
+    var sFileContainerId = $this._getFileContainerId(iFileId);
+    bx_loading(sFileContainerId, true);
+
+    $.post(sUrl, {_t: escape(new Date())}, function (sMsg) {
+        bx_loading(sFileContainerId, false);
+        if ('ok' == sMsg) {
+            $('#' + sFileContainerId).slideUp('slow', function () {
+                $(this).remove();
+            });
+        } else {
+            $('#' + this._sResultContainerId).prepend($this._sTemplateErrorGhosts.replace('{error}', sMsg));
+        }        
+    });
+};
+
+BxDolUploaderSimple.prototype._clearErrors = function () {
+    $('#' + this._sPopupContainerId + ' #' + this._sErrorsContainerId).html('');
+    this._isErrorShown = false;
+}
+
+BxDolUploaderSimple.prototype._showError = function (s, bAppend) {
+    if (s == undefined || !s.length)
+        return;
+    if (!bAppend)
+        $('#' + this._sPopupContainerId + ' #' + this._sErrorsContainerId).html(this._sTemplateError.replace('{error}', s));
+    else
+        $('#' + this._sPopupContainerId + ' #' + this._sErrorsContainerId).prepend(this._sTemplateError.replace('{error}', s));
+    this._isErrorShown = true;
+};
+
+BxDolUploaderSimple.prototype._getFileContainerId = function (iFileId) {
+	return 'bx-uploader-file-' + this._sStorageObject + '-' + iFileId;
+};
+
+BxDolUploaderSimple.prototype._getUrlWithStandardParams = function () {
+    return sUrlRoot + 'storage_uploader.php?uo=' + this._sUploaderObject + '&so=' + this._sStorageObject + '&uid=' + this._sUniqId;
+}
+
+BxDolUploaderSimple.prototype._lockPageFromLeaving = function () {
+    $(window).bind('beforeunload', function (e) {
+        var e = e || window.event;
+        // for ie, ff
+        e.returnValue = _t('_sys_uploader_confirm_leaving_page');
+        // for webkit
+        return _t('_sys_uploader_confirm_leaving_page');
+    });
+}
+
+BxDolUploaderSimple.prototype._unlockPageFromLeaving = function () {
+    $(window).unbind('beforeunload');
+}
+
+BxDolUploaderSimple.prototype._loading = function (bShowProgress, bShowForm) {
+
+    var eForm = $('#' + this._sFormContainerId + ' .bx-uploader-files-list');
+    var eBtn = $('#' + this._sFormContainerId + ' .bx-btn-primary');
+
+    if (bShowForm) {
+        if (null != this._eForm) {
+            eForm.find('.bx-uploader-simple-file').filter(':not(:first)').remove();
+            if ('undefined' !== typeof(this._eForm.reset)) 
+                this._eForm.reset();
+        }
+        eForm.show();
+        eBtn.show();
+    } else {
+        eForm.hide();
+        eBtn.hide();
+    }
+
+    bx_loading($('#' + this._sFormContainerId + ' .bx-uploader-loading').get(0), bShowProgress);
+}
+
+/**
+ * HTML5 Uploader js class
+ */
+function BxDolUploaderHTML5 (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe';
+    this._eForm = null;    
+
+    this._sDivId = 'bx-form-input-files-' + sUniqId + '-div-' + this._sUploaderObject;
+
+    this._sFocusDivId = 'bx-form-input-files-' + sUniqId + '-focus-' + this._sUploaderObject;
+
+    this._uploader = null;
+
+    this.initUploader = function (o) {
+
+        var $this = this;
+
+        if (null != this._uploader)
+            this._uploader = null;
+
+        var _options = {
+            url: sUrlRoot + 'storage_uploader.php',
+            uploadMultiple: false,
+            timeout: 0,
+            maxFiles: $this.isMultiple() ? 50 : 1,
+            params: {
+                uo: this._sUploaderObject,
+                so: this._sStorageObject,
+                uid: this._sUniqId,
+                m: this._isMultiple ? 1 : 0,
+                c: this._iContentId,
+                p: this._isPrivate,
+                a: "upload"
+            },
+        };
+
+        this._uploader = new Dropzone('#' + this._sDivId, $.extend({}, _options, o));
+        
+        this._uploader.on('queuecomplete', function (file) {
+            $this.onUploadCompleted('');
+        });
+        this._uploader.on('addedfile', function (file) {
+            $this.onBeforeUpload('');
+        });        
+        this._uploader.on('totaluploadprogress', function (uploadProgress) {
+            $this.onProgress({
+                total: uploadProgress
+            });
+        });
+        this._uploader.on('success', function (oFile, sJSON) {
+            var o = null;
+            try {
+                o = JSON.parse(sJSON);
+            } 
+            catch (e) {}
+
+            if (o && 'undefined' !== typeof(o.error)) {
+                $this._uploader.removeFile(oFile);
+                $this._showError(o.error, true);
+            }
+        });
+        this._uploader.on('error', function (oFile, sErrorMsg) {
+            $this._uploader.removeFile(oFile);
+            $this._showError(sErrorMsg, true);
+        });
+
+        this.initPasteEditor();
+    }
+
+    this.onUploadCompleted = function (sErrorMsg) {        
+
+        if (sErrorMsg.length)
+            this._showError(sErrorMsg);        
+        
+        if (0 == this._uploader.getQueuedFiles().length && 0 == this._uploader.getUploadingFiles().length) {
+            this._isUploadsInProgress = false;
+            this.restoreGhosts();
+            if (!this._isErrorShown) {
+                $('#' + this._sPopupContainerId).dolPopupHide({});
+                this._uploader.removeAllFiles();
+            }
+        }
+    }
+
+    this.cancelAll = function () {
+        this._uploader.removeAllFiles(true);
+    }
+
+    this.onBeforeUpload = function (params) {
+        this._isUploadsInProgress = true;
+        this._clearErrors();
+    }
+
+    this.onProgress = function (params) {
+
+    }
+
+    this.onClickCancel = function () {
+        this._uploader.removeAllFiles(true);
+        BxDolUploaderSimple.prototype._clearErrors.call(this);
+        BxDolUploaderSimple.prototype.onClickCancel.call(this);
+    }
+
+    this.onShowPopup = function () {
+        var $this = this;
+        setTimeout(function () {
+            $this.focusPasteEditor();
+        }, 200);
+    }
+
+    this.focusPasteEditor = function () {
+        $('#' + this._sFocusDivId).focus();
+    }
+
+    this.initPasteEditor = function () {
+        var $this = this;
+        $('#' + this._sFocusDivId).on('paste', function (e) {
+            var aFiles = [];
+            if (e.type == 'paste') {
+                var aItems = (e.clipboardData || e.originalEvent.clipboardData).items;
+                
+                for (var i in aItems) {
+                    var oItem = aItems[i];
+                    if (oItem.kind === 'file')
+                        aFiles.push(oItem.getAsFile());
+                }
+                $this._uploader.handleFiles(aFiles);
+            }
+        });
+    }
+}
+
+BxDolUploaderHTML5.prototype = BxDolUploaderSimple.prototype;
+
+/**
+ * Crop Image Uploader js class
+ */
+function BxDolUploaderCrop (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._eForm = null;
+
+    this.initUploader = function (oOptions) {
+
+        var $this = this;
+
+        var aExt = ['jpg', 'jpeg', 'png', 'gif'];
+
+        var eCroppie = $("#" + this._sFormContainerId + " .bx-croppie-element").croppie(oOptions);
+
+		$("#" + this._sFormContainerId + ' .bx-crop-rotate').on('click', function(ev) {
+			eCroppie.croppie('rotate', parseInt($(this).data('deg')));
+		});
+        
+        $("#" + this._sFormContainerId + " input[name=f]").on("change", function() {
+            var input = this;            
+
+            if (input.files && input.files[0]) {
+
+                var m = input.files[0].name.match(/\.([A-Za-z0-9]+)$/);
+
+                if (2 != m.length || -1 == aExt.indexOf(m[1].toLowerCase())) {                    
+                    $(input).replaceWith($(input).val('').clone(true));
+                    $this._showError(_t('_sys_uploader_crop_wrong_ext'));
+                    return;
+                }
+
+                $this._clearErrors();
+
+                var reader = new FileReader()
+
+                reader.onload = function(e) {
+                    eCroppie.croppie('bind', {
+                        url: e.target.result
+                    });
+                    $("#" + $this._sFormContainerId + " .bx-croppie-element").addClass('ready');
+                    $("#" + $this._sFormContainerId + " .bx-crop-action").removeClass('bx-btn-disabled');
+                    $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename', input.files[0].name.replace(/(\.[A-Za-z0-9]+)$/, '.jpg'));
+                }
+                reader.readAsDataURL(input.files[0]);
+            }
+        });
+
+        $("#" + this._sFormContainerId + " .bx-crop-upload").on('click', function(ev) {
+            eCroppie.croppie('result', {
+                type: 'canvas',
+                size: 'original',
+                format: 'jpeg',
+                quality: '0.85',
+            }).then(function(resp) {
+                var fd = new FormData();
+
+                fd.append("f", dataURItoBlob(resp), $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename'));
+                $.each(oOptions.bx_form, function (sName) {
+                    fd.append(sName, this);
+                });
+                
+                $this.onBeforeUpload(fd);
+
+                $.ajax({
+                    url: sUrlRoot + 'storage_uploader.php',
+                    type: "POST",
+                    processData: false,
+                    contentType: false,
+                    data: fd,
+                    success: function(data) {
+                        eval(data);
+                    },
+                    error: function() {
+                        $this._showError(_t('_sys_uploader_crop_err_upload'));
+                    }
+                })
+
+            });
+        });
+
+        function dataURItoBlob(dataURI) {
+            var split = dataURI.split(','),
+                dataTYPE = split[0].match(/:(.*?);/)[1],
+                binary = atob(split[1]),
+                array = [];
+                
+            for (var i = 0; i < binary.length; i++) 
+                array.push(binary.charCodeAt(i));
+
+            return new Blob([new Uint8Array(array)], {
+                type: dataTYPE
+            });
+        }
+
+        
+    };
+}
+
+BxDolUploaderCrop.prototype = BxDolUploaderSimple.prototype;
+
+
+/**
+ * Record Video Uploader js class
+ */
+function BxDolUploaderRecordVideo (sUploaderObject, sStorageObject, sUniqId, options) {
+    this._camera = null;
+    this._blob = null;
+    this._recorder = null
+
+    this._audio_bitrate = undefined !== options.audio_bitrate ? parseInt(options.audio_bitrate) : 128000;
+    this._video_bitrate = undefined !== options.video_bitrate ? parseInt(options.video_bitrate) : 1000000;
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this.showUploaderForm = function() {
+        if (typeof MediaRecorder == 'undefined' || !('requestData' in MediaRecorder.prototype)) {
+            bx_alert(_t('_sys_uploader_unsupported_browser'));
+            return;
+        }
+
+        BxDolUploaderSimple.prototype.showUploaderForm.call(this);
+    }
+
+    this.onBeforeShowPopup = function() {
+        this._blob = null;
+        this._camera = null;
+        this._recorder = null
+        this._clearErrors();
+
+        $('#bx-upoloader-recording-preview').hide();
+        $('#bx-upoloader-camera-capture').show();
+
+        $('#' + this._sFormContainerId + ' .bx-uploader-record-video-controls').hide();
+    }
+
+    this.onShowPopup = function () {
+        var $this = this;
+        navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(camera) {
+            $this._camera = camera;
+            $this.showCameraCapture();
+
+            $('#' + $this._sFormContainerId + ' .bx-uploader-recording-start').show();
+            $('#' + $this._sFormContainerId + ' .bx-uploader-recording-stop').hide();
+            $('#' + $this._sFormContainerId + ' .bx-uploader-record-video-controls').show();
+        }).catch(function(error) {
+            $this._showError(_t('_sys_uploader_camera_capture_failed'));
+        });
+    }
+
+    this.onClickCancel = function () {
+        this.releaseCamera();
+
+        BxDolUploaderSimple.prototype.onClickCancel.call(this);
+    }
+
+    this.startRecording = function() {
+        this._recorder = RecordRTC(this._camera, {
+            type: 'video',
+            audioBitsPerSecond: this._audio_bitrate,
+            videoBitsPerSecond: this._video_bitrate,
+            disableLogs: true
+        });
+
+        if (!this._recorder) {
+            this._showError(_t('_sys_uploader_unsupported_browser'));
+            return;
+        }
+
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').hide();
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').show();
+
+        this._recorder.startRecording();
+
+        this.showCameraCapture();
+    }
+
+    this.stopRecording = function(bSubmitWhenReady) {
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').show();
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').hide();
+
+        var $this = this;
+        this._recorder.stopRecording(function(){
+            $this._blob = $this._recorder.getBlob();
+            $this.showRecordingPreview();
+
+            $this._recorder.destroy();
+            $this._recorder = null;
+
+            if (bSubmitWhenReady)
+                $this.submitRecording($('#' + $this._sFormContainerId + ' form').get(0));
+        });
+    }
+
+    this.showCameraCapture = function() {
+        var video = $('#bx-upoloader-recording-preview').hide().get(0);
+        if (video.pause !== 'undefined') video.pause();
+        video.removeAttribute('src');
+
+        video = $('#bx-upoloader-camera-capture').show().get(0);
+        video.srcObject = this._camera;
+        video.muted = true;
+        video.volume = 0;
+        $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html('');
+    }
+
+    this.showRecordingPreview = function() {
+        $('#bx-upoloader-camera-capture').hide();
+        $('#bx-upoloader-recording-preview').show().get(0).src = URL.createObjectURL(this._blob);
+
+        var mbytes = (this._blob.size/1024/1024).toFixed(2);
+        $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html(mbytes + ' ' + _t('_sys_uploader_record_video_mb'));
+    }
+
+    this.submitRecording = function(form) {
+        if (this._recorder) {
+            this.stopRecording(true);
+            return;
+        }
+
+        this.onBeforeUpload(form);
+
+        var data = new FormData(form);
+        if (this._blob != null) data.append("f[]", this._blob, new Date().toISOString() + '.webm');
+
+        var $this = this;
+        $.ajax({
+            url: $(form).attr('action'),
+            type: "POST",
+            data: data,
+            processData: false,
+            contentType: false,
+            success:function(sErrorMsg, textStatus, jqXHR) {
+                $this.onUploadCompleted(sErrorMsg);
+                if (!sErrorMsg.length) {
+                    $this.releaseCamera();
+                }
+            },
+        });
+    }
+
+    this.releaseCamera = function() {
+        //stop playing recorded file
+        var video = $('#bx-upoloader-recording-preview').get(0);
+        if (video.pause != 'undefined') video.pause();
+        video.removeAttribute('src');
+
+        if (this._recorder) {
+            this._recorder.destroy();
+            this._recorder = null;
+        }
+
+        if (this._camera) {
+            if (typeof this._camera.stop != 'undefined') this._camera.stop();
+            this._camera.getTracks().forEach(function (track) {
+                if (track.readyState == 'live') {
+                    track.stop();
+                }
+            });
+        }
+    }
+}
+
+BxDolUploaderRecordVideo.prototype = BxDolUploaderSimple.prototype;
+
+/** @} */

+ 250 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/classes/BxDolVoteReactions.js

@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolVoteReactions(oOptions)
+{
+    BxDolVote.call(this, oOptions);
+
+    this._iTimeoutShowId = 0;
+    this._iTimeoutShowDelay = 750;
+    this._iTimeoutHideId = 0;
+    this._iTimeoutHideDelay = 1000;
+    this._fOnVoteIn = null;
+    this._fOnVoteOut = null;
+    this._fOnDoPopupIn = null;
+    this._fOnDoPopupOut = null;
+
+    this._sClassDo = 'bx-vote-do-vote';
+    this._sClassDoVoted = 'bx-vote-voted';
+
+    var $this = this;
+    $(document).ready(function() {
+        $this.initVote();
+    });
+}
+
+BxDolVoteReactions.prototype = Object.create(BxDolVote.prototype);
+BxDolVoteReactions.prototype.constructor = BxDolVoteReactions;
+
+BxDolVoteReactions.prototype.initVote = function()
+{
+    var $this = this;
+
+    this._fOnVoteIn = function() {
+        $this.onVoteIn(this);
+    };
+
+    this._fOnVoteOut = function() {
+        $this.onVoteOut(this);
+    };
+
+    $('#' + this._aHtmlIds['main'] + ' .' + this._sClassDo).hover(this._fOnVoteIn, this._fOnVoteOut);
+};
+
+BxDolVoteReactions.prototype.vote = function(oLink, iValue, sReaction, onComplete)
+{
+    var $this = this;
+    var oParams = this._getDefaultParams();
+    oParams['action'] = 'Vote';
+    oParams['value'] = iValue;
+    oParams['reaction'] = sReaction;
+
+    $.post(
+    	this._sActionsUrl,
+    	oParams,
+    	function(oData) {
+            if(oData && oData.message != undefined)
+                bx_alert(oData.message, function() {
+                    $this.onVote(oLink, oData, onComplete);
+                });
+            else
+                $this.onVote(oLink, oData, onComplete);
+        },
+        'json'
+    );
+};
+
+BxDolVoteReactions.prototype.onVote = function (oLink, oData, onComplete)
+{
+    var $this = this;
+
+    if(oData && oData.code != 0)
+        return;
+    
+    $('#' + this._aHtmlIds['do_popup']).dolPopupHide({});
+
+    oLink = $('.' + this._aHtmlIds['main'] + ' .' + this._sClassDo);
+
+    //--- Update Do button.
+    oLink.each(function() {
+        if(oData && oData.label_icon)
+            $(this).find('.sys-action-do-icon .sys-icon').attr('class', 'sys-icon ' + oData.label_icon);
+
+        if(oData && oData.label_title) {
+            $(this).attr('title', oData.label_title);
+            $(this).find('.sys-action-do-text').html(oData.label_title);
+        }
+
+        if(oData && oData.label_click)
+            $(this).attr('onclick', 'javascript:' + oData.label_click)
+
+        if(oData && oData.disabled)
+            $(this).removeAttr('onclick').addClass($(this).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-vote-disabled');
+        else
+            $(this).toggleClass($this._sClassDoVoted);
+    });
+
+    //--- Update Counter.
+    var oCounter = this._getCounter(oLink);
+    if(oCounter && oCounter.length > 0) {
+        oCounter.filter('.' + oData.reaction).each(function() {
+            $(this).html(oData.countf).toggleClass('bx-vc-hidden', !oData.count);            
+        });
+
+        //--- Update Total.
+        if(oData.total)
+            oCounter.filter('.total-count').each(function() {
+                $(this).html(oData.total.countf).toggleClass('bx-vc-hidden', !oData.total.count);
+            });
+        
+        oCounter.parents('.' + this._sSP + '-counter-wrapper:first').toggleClass('bx-vc-hidden', !oData.count && !oData.total.count);
+    }
+
+    if(typeof onComplete == 'function')
+        onComplete(oLink, oData);
+};
+
+BxDolVoteReactions.prototype.onVoteIn = function(oLink)
+{
+    if($(oLink).hasClass(this._sClassDoVoted))
+        return;
+
+    if(this._iTimeoutHideId)
+        clearTimeout(this._iTimeoutHideId);
+
+    var oPopup = this.getDoPopup();
+    if(oPopup !== false)
+        return;
+
+    this._iTimeoutShowId = setTimeout(function() {
+        $(oLink).click();
+    }, this._iTimeoutShowDelay);
+};
+
+BxDolVoteReactions.prototype.onVoteOut = function(oLink)
+{
+    if($(oLink).hasClass(this._sClassDoVoted))
+        return;
+
+    if(this._iTimeoutShowId)
+        clearTimeout(this._iTimeoutShowId);
+
+    this.hideDoPopup();
+};
+
+BxDolVoteReactions.prototype.getDoPopup = function()
+{
+    var oPopup = $('#' + this._aHtmlIds['do_popup'] + ':visible');
+    return oPopup.length > 0 && oPopup.hasClass('bx-popup-applied') ? oPopup : false;
+};
+
+BxDolVoteReactions.prototype.toggleDoPopup = function(oLink, iValue)
+{
+    var $this = this;
+    var oParams = this._getDefaultParams();
+    oParams['action'] = 'GetDoVotePopup';
+
+    if(this._iTimeoutShowId)
+        clearTimeout(this._iTimeoutShowId);
+
+    $(oLink).dolPopupAjax({
+        id: {value: this._aHtmlIds['do_popup'], force: true}, 
+        url: bx_append_url_params(this._sActionsUri, oParams),
+        value: iValue,
+        onShow: function(oPopup) {
+            $this.onDoPopupShow(oPopup);
+        },
+        onHide: function(oPopup) {
+            $this.onDoPopupHide(oPopup);
+        }
+    });
+};
+
+BxDolVoteReactions.prototype.hideDoPopup = function()
+{
+    var oPopup = this.getDoPopup();
+    if(!oPopup)
+        return;
+
+    this._iTimeoutHideId = setTimeout(function() {
+        oPopup.dolPopupHide();
+    }, this._iTimeoutHideDelay);
+};
+
+BxDolVoteReactions.prototype.onDoPopupShow = function(oPopup)
+{
+    var $this = this;
+
+    this._fOnDoPopupIn = function() {
+        $this.onDoPopupIn(this);
+    };
+
+    this._fOnDoPopupOut = function() {
+        $this.onDoPopupOut(this);
+    };
+
+    $(oPopup).hover(this._fOnDoPopupIn, this._fOnDoPopupOut);
+};
+
+BxDolVoteReactions.prototype.onDoPopupHide = function(oPopup)
+{
+    $(oPopup).unbind('mouseenter', this._fOnDoPopupIn).unbind('mouseleave', this._fOnDoPopupOut);
+};
+
+BxDolVoteReactions.prototype.onDoPopupIn = function(oPopup)
+{
+    if(this._iTimeoutHideId)
+        clearTimeout(this._iTimeoutHideId);
+};
+
+BxDolVoteReactions.prototype.onDoPopupOut = function(oPopup)
+{
+    this.hideDoPopup();
+};
+
+BxDolVoteReactions.prototype.toggleByPopup = function(oLink, sReaction)
+{
+    var oParams = this._getDefaultParams();
+    oParams['action'] = 'GetVotedBy';
+    if(sReaction)
+        oParams['reaction'] = sReaction;
+
+    $(oLink).dolPopupAjax({
+        id: {value: this._aHtmlIds['by_popup'], force: true}, 
+        url: bx_append_url_params(this._sActionsUri, oParams),
+        removeOnClose: true
+    });
+};
+
+BxDolVoteReactions.prototype.changeVotedBy = function(oLink, sReaction)
+{
+    var oContent = $('#' + this._aHtmlIds['by_popup'] + ' .' + this._sSP + '-bls-content');
+    if(oContent.length > 0)
+        oContent.children(':visible').hide().siblings('.' + this._sSP + '-bl-' + sReaction).show();
+};
+
+BxDolVoteReactions.prototype._getCounter = function(oElement)
+{
+    var oCounter = BxDolVote.prototype._getCounter.call(this, oElement);
+    if(oCounter && oCounter.length > 0)
+        return oCounter;
+
+    return $('.' + this._aHtmlIds['counter']).find('.' + this._sSP + '-counter');
+};
+
+/** @} */

+ 1158 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/functions.js

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

+ 885 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/jquery.dolPopup.js

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

+ 333 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/js/jquery.webForms.js

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

+ 108 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/params.inc.php

@@ -0,0 +1,108 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_START', microtime ());
+
+//--- Main URLs ---//
+define('BX_DOL_URL_PLUGINS', BX_DOL_URL_ROOT . 'plugins/');
+define('BX_DOL_URL_PLUGINS_PUBLIC', BX_DOL_URL_ROOT . 'plugins_public/');
+define('BX_DOL_URL_MODULES', BX_DOL_URL_ROOT . 'modules/');
+define('BX_DOL_URL_CACHE_PUBLIC', BX_DOL_URL_ROOT . 'cache_public/');
+define('BX_DOL_URL_BASE', BX_DOL_URL_ROOT . 'template/');
+
+//--- Main Pathes ---//
+define('BX_DIRECTORY_PATH_INC', BX_DIRECTORY_PATH_ROOT . 'inc/');
+define('BX_DIRECTORY_PATH_BASE', BX_DIRECTORY_PATH_ROOT . 'template/');
+define('BX_DIRECTORY_PATH_CACHE', BX_DIRECTORY_PATH_ROOT . 'cache/');
+define('BX_DIRECTORY_PATH_CACHE_PUBLIC', BX_DIRECTORY_PATH_ROOT . 'cache_public/');
+define('BX_DIRECTORY_PATH_CLASSES', BX_DIRECTORY_PATH_ROOT . 'inc/classes/');
+define('BX_DIRECTORY_PATH_PLUGINS', BX_DIRECTORY_PATH_ROOT . 'plugins/');
+define('BX_DIRECTORY_PATH_PLUGINS_PUBLIC', BX_DIRECTORY_PATH_ROOT . 'plugins_public/');
+define('BX_DIRECTORY_PATH_MODULES', BX_DIRECTORY_PATH_ROOT . 'modules/');
+define('BX_DIRECTORY_PATH_TMP', BX_DIRECTORY_PATH_ROOT . 'tmp/');
+define('BX_DIRECTORY_PATH_LOGS', BX_DIRECTORY_PATH_ROOT . 'logs/');
+define('BX_DIRECTORY_STORAGE', BX_DIRECTORY_PATH_ROOT . 'storage/');
+
+//--- System settings ---//
+define('BX_DOL_DIR_RIGHTS', 0777);
+define('BX_DOL_FILE_RIGHTS', 0666);
+
+define('BX_DOL_STORAGE_OBJ_IMAGES', 'sys_images');
+
+if (!defined('BX_DOL_INT_MAX'))
+    define('BX_DOL_INT_MAX', 2147483647);
+
+define('BX_DOL_TRANSCODER_OBJ_ICON_APPLE', 'sys_icon_apple');
+define('BX_DOL_TRANSCODER_OBJ_ICON_FACEBOOK', 'sys_icon_facebook');
+define('BX_DOL_TRANSCODER_OBJ_ICON_FAVICON', 'sys_icon_favicon');
+define('BX_DOL_TRANSCODER_OBJ_COVER', 'sys_cover');
+define('BX_DOL_TRANSCODER_OBJ_COVER_UNIT_PROFILE', 'sys_cover_unit_profile');
+
+//--- Module types ---//
+if (!defined('BX_DOL_MODULE_TYPE_MODULE')) {
+    define('BX_DOL_MODULE_TYPE_MODULE', 'module');
+    define('BX_DOL_MODULE_TYPE_LANGUAGE', 'language');
+    define('BX_DOL_MODULE_TYPE_TEMPLATE', 'template');
+}
+
+//--- Payment settings ---//
+define('BX_PAYMENT_TYPE_SINGLE', 'single');
+define('BX_PAYMENT_TYPE_RECURRING', 'recurring');
+
+define('BX_PAYMENT_EMPTY_ID', 0);
+
+//--- Studio settings ---//
+define('BX_DOL_STUDIO_FOLDER', 'studio');
+
+define('BX_DOL_URL_STUDIO', BX_DOL_URL_ROOT . BX_DOL_STUDIO_FOLDER . '/');
+define('BX_DOL_URL_STUDIO_BASE', BX_DOL_URL_STUDIO . 'template/');
+
+define('BX_DOL_DIR_STUDIO', BX_DIRECTORY_PATH_ROOT . BX_DOL_STUDIO_FOLDER . '/');
+define('BX_DOL_DIR_STUDIO_INC', BX_DOL_DIR_STUDIO . 'inc/');
+define('BX_DOL_DIR_STUDIO_CLASSES', BX_DOL_DIR_STUDIO . 'classes/');
+define('BX_DOL_DIR_STUDIO_BASE', BX_DOL_DIR_STUDIO . 'template/');
+
+//--- User Roles ---//
+define('BX_DOL_ROLE_GUEST', 0);
+define('BX_DOL_ROLE_MEMBER', 1);
+define('BX_DOL_ROLE_ADMIN', 2);
+
+//--- User Roles in Studio ---//
+define('BX_DOL_STUDIO_ROLE_MASTER', 1);
+define('BX_DOL_STUDIO_ROLE_OPERATOR', 2);
+
+//--- Profile Statuses ---//
+define('BX_PROFILE_STATUS_SUSPENDED', 'suspended'); ///< profile status - suspended, profile is syspended by admin/moderator and usually can't access the site
+define('BX_PROFILE_STATUS_ACTIVE', 'active'); ///< profile status - active, profile is active on the site
+define('BX_PROFILE_STATUS_PENDING', 'pending'); ///< profile status - pending, default method of approving is manual approving
+
+//--- Account settings ---//
+define('BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_ACCOUNT_ID', '2FA_LOGIN_ACCOUNT_ID');
+define('BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_IS_REMEMBER', '2FA_LOGIN_IS_REMEMBER');
+define('BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE', 'PHONE_ACTIVATEION_CODE');
+define('BX_ACCOUNT_CONFIRMATION_NONE', 'none');
+define('BX_ACCOUNT_CONFIRMATION_EMAIL', 'email');
+define('BX_ACCOUNT_CONFIRMATION_PHONE', 'phone');
+define('BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE', 'email_and_phone');
+
+//--- Include files needed for basic functionality ---//
+require_once(BX_DIRECTORY_PATH_CLASSES . 'BxDol.php');
+require_once(BX_DIRECTORY_PATH_INC . 'utils.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . 'version.inc.php');
+
+spl_autoload_register('bx_autoload');
+
+BxDolDb::getInstance()->cacheParams();
+
+require_once(BX_DIRECTORY_PATH_INC . 'profiles.inc.php');
+
+$o = new BxDolAlerts('system', 'begin', 0);
+$o->alert();
+
+/** @} */

+ 310 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/profiles.inc.php

@@ -0,0 +1,310 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @return corrently logged in profile id
+ */
+function bx_get_logged_profile_id ()
+{
+    $o = BxDolProfile::getInstance();
+    return $o ? $o->id() : false;
+}
+
+/**
+ * @return true if user is logged in
+ */
+function isLogged()
+{
+    return getLoggedId() != 0;
+}
+
+/**
+ * @return logged in account id
+ */
+function getLoggedId()
+{
+    return isset($_COOKIE['memberID']) && (!empty($GLOBALS['logged']['member']) || !empty($GLOBALS['logged']['admin'])) ? (int)$_COOKIE['memberID'] : 0;
+}
+
+/**
+ * @return logged in account password
+ */
+function getLoggedPassword()
+{
+    return isset($_COOKIE['memberPassword']) && ($GLOBALS['logged']['member'] || $GLOBALS['logged']['admin']) ? $_COOKIE['memberPassword'] : '';
+}
+
+/**
+ * It checks if account role is member.
+ */
+function isMember($iId = 0)
+{
+    return isRole(BX_DOL_ROLE_MEMBER, $iId);
+}
+
+if (!function_exists("isAdmin")) {
+    /**
+     * @return true if account is admin
+     */
+    function isAdmin($iId = 0)
+    {
+        if (!$iId && isset($GLOBALS['logged']['admin']) && $GLOBALS['logged']['admin']) // easier check for currently logged in user
+            return true;
+        return isRole(BX_DOL_ROLE_ADMIN, $iId);
+    }
+}
+
+/**
+ * Check user role
+ * @param $iRole role to check user for
+ * @param $iId optional account id, if it isn't specified then curently logged in account is used
+ * @return true if user is in the provided role
+ */
+function isRole($iRole, $iId = 0)
+{
+    if (!(int)$iId)
+        $iId = getLoggedId();
+
+    $oAccount = BxDolAccount::getInstance($iId);
+    if (!$oAccount)
+        return false;
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    if (!$aAccountInfo)
+        return false;
+
+    if (!((int)$aAccountInfo['role'] & $iRole))
+        return false;
+
+    return true;
+}
+
+/**
+ * Login user by setting necessary cookies
+ * @param $iId account id
+ * @param $bRememberMe remember session
+ * @return false if id isn't correct or array of user info if user was logged in
+ */
+function bx_login($iId, $bRememberMe = false)
+{
+    $oAccountQuery = BxDolAccountQuery::getInstance();
+
+    $sPassword = $oAccountQuery->getPassword($iId);
+    if (!$sPassword)
+        return false;
+
+    $aUrl = parse_url(BX_DOL_URL_ROOT);
+    $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+    $sHost = '';
+    $iCookieTime = $bRememberMe ? time() + 60 * getParam('sys_session_lifetime_in_min') : 0;
+    setcookie("memberID", $iId, $iCookieTime, $sPath, $sHost);
+    $_COOKIE['memberID'] = $iId;
+    setcookie("memberPassword", $sPassword, $iCookieTime, $sPath, $sHost, false, true /* http only */);
+    $_COOKIE['memberPassword'] = $sPassword;
+
+    BxDolSession::getInstance()->setUserId($iId);
+
+    $oAccountQuery->updateLoggedIn($iId);
+
+    bx_alert('account', 'login',  $iId);
+
+    return $oAccountQuery->getInfoById($iId);
+}
+
+/**
+ * Logout user by removing cookies
+ */
+function bx_logout($bNotify = true)
+{
+    if ($bNotify && isMember())
+        bx_alert('account', 'logout', (int)$_COOKIE['memberID']);
+
+    $aUrl = parse_url(BX_DOL_URL_ROOT);
+    $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+    setcookie('memberID', '', time() - 96 * 3600, $sPath);
+    setcookie('memberPassword', '', time() - 96 * 3600, $sPath);
+
+    unset($_COOKIE['memberID']);
+    unset($_COOKIE['memberPassword']);
+
+    BxDolSession::getInstance()->destroy();
+}
+
+/**
+ * Check if user is logged in (necessary cookies are present) and set some global variables
+ */
+function check_logged()
+{
+    $aAccTypes = array(
+       BX_DOL_ROLE_ADMIN => 'admin',
+       BX_DOL_ROLE_MEMBER => 'member'
+    );
+
+    $bID = isset($_COOKIE['memberID']);
+    $sID = $bID ? bx_process_input($_COOKIE['memberID']) : false;
+
+    $bPassword = isset($_COOKIE['memberPassword']);
+    $sPassword = $bPassword ? bx_process_input($_COOKIE['memberPassword']) : false;
+
+    $bLogged = false;
+    foreach ($aAccTypes as $iRole => $sValue) {
+        if ($GLOBALS['logged'][$sValue] = ($sID && !bx_check_login($sID, $sPassword, $iRole))) {
+            BxDolSession::getInstance();
+            $bLogged = true;
+            break;
+        }
+    }
+
+    if($bID && $bPassword && $bLogged)
+        bx_alert('account', 'logged', getLoggedId());
+
+    if(($bID || $bPassword) && !$bLogged)
+        bx_logout(false);
+}
+
+/**
+ * Check unencrypted password
+ * @return empty string on success or error string on error
+ */
+function bx_check_password($sLogin, $sPassword, $iRole = BX_DOL_ROLE_MEMBER)
+{
+    $oAccount = BxDolAccount::getInstance($sLogin);
+    if (!$oAccount) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+    
+    if ($oAccount->isLocked()){
+        bx_import('BxDolLanguages');
+		return _t("_sys_txt_login_locked", BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password'));
+	}
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    $sPassCheck = encryptUserPwd($sPassword, $aAccountInfo['salt']);
+
+	// regenerate password using another encrypt function if necessary
+	bx_alert('system', 'encrypt_password_after', 0, false, array(
+            'info' => $aAccountInfo,
+			'pwd' => $sPassword,
+            'password' => &$sPassCheck,
+        ));
+    
+    if ($sErrorMsg = bx_check_login($aAccountInfo['id'], $sPassCheck, $iRole)){
+        $iMaxLoginAttempts = getParam('sys_account_limit_incorrect_login_attempts');
+        if ($iMaxLoginAttempts >0){
+            $oAccountQuery = BxDolAccountQuery::getInstance();
+            if ($aAccountInfo['login_attempts'] >= $iMaxLoginAttempts){
+                $oAccountQuery->lockAccount($aAccountInfo['id'], 1);
+                bx_import('BxDolLanguages');
+                return _t("_sys_txt_login_locked", BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password'));
+            }
+            else{
+                $oAccountQuery->updateAttemptsCounter($aAccountInfo['id']);
+            }
+        }
+        return $sErrorMsg;
+    }
+
+    // Admin can always login even if he is blocked/banned/suspended/etc
+    if (isAdmin($aAccountInfo['id']))
+        return '';
+
+    $sErrorMsg = '';
+    bx_alert('account', 'check_login',  $aAccountInfo['id'], false, array('error_msg' => &$sErrorMsg));
+    
+    return $sErrorMsg;
+}
+
+/**
+ * Check encrypted password (ex., from Cookie)
+ * @return empty string on success or error string on error
+ */
+function bx_check_login($iID, $sPassword, $iRole = BX_DOL_ROLE_MEMBER)
+{
+    $oAccount = BxDolAccount::getInstance((int)$iID);
+
+    // If no such account available
+    if (!$oAccount) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    // If password is incorrect
+    if (strcmp($aAccountInfo['password'], $sPassword) !== 0) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+
+    // If wrong account role
+    if (!((int)$aAccountInfo['role'] & $iRole)) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_invalid_role");
+    }
+
+    return '';
+}
+
+/**
+ * Declare that content is require user authoriztion and display login form if user isn't logged in
+ * @param $bStudio require webmaster authorization
+ */
+function bx_require_authentication ($bStudio = false, $bAjaxMode = false, $sForceRelocate = '')
+{
+    $iRole = BX_DOL_ROLE_MEMBER;
+    if ($bStudio)
+        $iRole = BX_DOL_ROLE_ADMIN;
+
+    $sID = isset($_COOKIE['memberID']) ? bx_process_input($_COOKIE['memberID']) : false;
+    $sPassword = isset($_COOKIE['memberPassword']) ? bx_process_input($_COOKIE['memberPassword']) : false;
+
+    if (bx_check_login($sID, $sPassword, $iRole)) {
+        bx_login_form($bStudio, $bAjaxMode, $sForceRelocate);
+    }
+
+    check_logged();
+}
+
+
+
+/**
+ * Display login form and exit
+ * @param $bStudio display login form for studio
+ * @param $bAjaxMode login form displayed via AJAX
+ * @param sForceRelocate forece relocate
+ */
+function bx_login_form($bStudio = false, $bAjaxMode = false, $sForceRelocate = '')
+{
+    if ($bStudio == 1) {
+        BxTemplStudioFunctions::getInstance()->getLoginForm();
+        exit;
+    }
+
+    $sFormCode = BxDolService::call('system', 'login_form', array('', $sForceRelocate), 'TemplServiceLogin');
+
+    if ($bAjaxMode) {
+        echo $GLOBALS['oFunctions']->transBox('', $sFormCode, false, true);
+        exit;
+    }
+
+    BxDolTemplate::getInstance()->setPageNameIndex (BX_PAGE_DEFAULT);
+    BxDolTemplate::getInstance()->setPageHeader (getParam('site_title') . ' ' . _t("_Member Login"));
+    BxDolTemplate::getInstance()->setPageContent ('page_main_code', DesignBoxContent(_t("_Member Login"), $sFormCode, BX_DB_PADDING_DEF));
+    BxDolTemplate::getInstance()->getPageCode();
+
+    exit;
+}
+
+check_logged();
+
+/** @} */

+ 2056 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/utils.inc.php

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

+ 12 - 0
upgrade/files/11.0.4-12.0.0.B1/files/inc/version.inc.php

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

+ 28 - 0
upgrade/files/11.0.4-12.0.0.B1/files/label.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+bx_import('BxDolLanguages');
+
+check_logged();
+
+$sAction = isset($_REQUEST['action']) && preg_match ('/^[A-Za-z_-]+$/', $_REQUEST['action']) ? bx_process_input($_REQUEST['action']) : '';
+
+$oLabel = BxDolLabel::getInstance();
+
+if($oLabel && $sAction) {
+    header('Content-Type: text/html; charset=utf-8');
+    $sMethod = 'action' . bx_gen_method_name($sAction);
+    if(method_exists($oLabel, $sMethod))
+        echo $oLabel->$sMethod();
+}
+
+/** @} */

+ 91 - 0
upgrade/files/11.0.4-12.0.0.B1/files/member.php

@@ -0,0 +1,91 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+bx_import('BxDolLanguages');
+
+if (isset($_POST['ID'])) { // login form is submitted
+
+    $oForm = BxDolForm::getObjectInstance('sys_login', 'sys_login');
+
+    bx_alert('account', 'before_login', 0, 0, array('form' => $oForm));
+
+    $oForm->initChecker();
+    $oForm->setRole(bx_get('role'));
+    $bLoginSuccess = $oForm->isSubmittedAndValid();
+
+    $bAjxMode = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') ? true : false;
+    if ($bAjxMode) {
+
+        if ($bLoginSuccess) {
+            $s = 'OK';
+            $oAccount = BxDolAccount::getInstance(trim($oForm->getCleanValue('ID')));
+            $aAccount = bx_login($oAccount->id(), ($oForm->getCleanValue('rememberMe') ? true : false));
+        }
+        else {
+            $s = $oForm->getLoginError();
+        }
+
+        if (isset($_SERVER['HTTP_ACCEPT'])) {
+            if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/json') || false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/javascript')) {
+                header('Content-type: application/json; charset=utf-8');
+                echo json_encode(['res' => $s, 'form' => $oForm->getCode()]);
+                exit;
+            }
+        }
+
+        header('Content-type: text/html; charset=utf-8');
+        echo $s;
+        exit;
+
+    } 
+    elseif ($bLoginSuccess) {
+        $sId = trim($oForm->getCleanValue('ID'));
+        $oAccount = BxDolAccount::getInstance($sId);
+        $aAccountInfo = $oAccount->getInfo();
+        if (
+            (getParam('sys_account_activation_2fa_enable') == 'on' && getParam('sys_twilio_gate_sid') != '' && getParam('sys_twilio_gate_token') != '' && getParam('sys_twilio_gate_from_number') != '') 
+            && (getParam('sys_account_activation_2fa_lifetime') == 0 || (time() - $aAccountInfo['logged'] > getParam('sys_account_activation_2fa_lifetime')))){
+            $oSession = BxDolSession::getInstance();
+            $oSession->setValue(BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_ACCOUNT_ID, trim($oForm->getCleanValue('ID')));
+            $oSession->setValue(BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_IS_REMEMBER, ($oForm->getCleanValue('rememberMe') ? true : false));
+            header('Location: ' . BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=login-step2'));
+        }
+        else{
+    	   
+            $aAccount = bx_login($oAccount->id(), ($oForm->getCleanValue('rememberMe') ? true : false));
+
+            $sUrlRelocate = $oForm->getCleanValue('relocate');
+            if (!$sUrlRelocate || 0 !== strncmp($sUrlRelocate, BX_DOL_URL_ROOT, strlen(BX_DOL_URL_ROOT)))
+                $sUrlRelocate = BX_DOL_ROLE_ADMIN == $oForm->getRole() ? BX_DOL_URL_STUDIO . 'launcher.php' : BX_DOL_URL_ROOT . 'member.php';
+
+            bx_alert('account', 'login_after', $oAccount->id(),  false, array(
+                'account' => $aAccount,
+                'url_relocate' => &$sUrlRelocate               
+            ));
+
+            BxDolTemplate::getInstance()->setPageNameIndex (BX_PAGE_TRANSITION);
+            BxDolTemplate::getInstance()->setPageHeader (_t('_Please Wait'));
+            BxDolTemplate::getInstance()->setPageContent ('page_main_code', MsgBox(_t('_Please Wait')));
+            BxDolTemplate::getInstance()->setPageContent ('url_relocate', bx_html_attribute($sUrlRelocate, BX_ESCAPE_STR_QUOTE));
+
+            BxDolTemplate::getInstance()->getPageCode();
+        exit;
+        }
+    }
+
+}
+
+bx_require_authentication();
+
+header('Location: ' . BX_DOL_URL_ROOT);
+
+/** @} */

+ 25 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/files/classes/BxBaseModFilesPrivacy.php

@@ -0,0 +1,25 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseFile Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModFilesPrivacy extends BxTemplPrivacy
+{
+    protected $MODULE;
+    protected $_oModule;
+
+    public function __construct($aOptions, $oTemplate = false)
+    {
+        parent::__construct($aOptions, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+    }
+}
+
+/** @} */

+ 357 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralDb.php

@@ -0,0 +1,357 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Module database queries
+ */
+class BxBaseModGeneralDb extends BxDolModuleDb
+{
+    protected $_oConfig;
+
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+        $this->_oConfig = $oConfig;
+    }
+
+    public function getContentInfoById ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_ENTRIES']) || empty($CNF['FIELD_ID']))
+            return array();
+
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = ?", $iContentId);
+        return $this->getRow($sQuery);
+    }
+
+    function getEntriesBy($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array()));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`" . $CNF['TABLE_ENTRIES'] . "`.*";
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['id'] = (int)$aParams['id'];
+
+                $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "` = :id";
+                break;
+
+            case 'author':
+                $aMethod['params'][1]['author'] = (int)$aParams['author'];
+
+                $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_AUTHOR'] . "` = :author";
+                break;
+
+            case 'conditions':
+                if(empty($aParams['conditions']))
+                    break;
+
+                if(is_array($aParams['conditions']))
+                    $sWhereClause .= $this->arrayToSQL($aParams['conditions'], ' AND ');
+                else if(is_string($aParams['conditions']) && !empty($aParams['bindings']) && is_array($aParams['bindings'])) {
+                    $sWhereClause .= $aParams['conditions'];
+                    
+                    if(!is_array($aMethod['params'][1]))
+                        $aMethod['params'][1] = array();
+
+                    $aMethod['params'][1] = array_merge($aMethod['params'][1], $aParams['bindings']);
+                }
+                break;
+
+            case 'search_ids':
+                $this->_getEntriesBySearchIds($aParams, $aMethod, $sSelectClause, $sJoinClause, $sWhereClause, $sOrderClause, $sLimitClause);
+                bx_alert('system', 'search_ids', 0, 0, array('module_name' => $this->_oConfig->getName(), 'params' => &$aParams, 'select_clause' => &$sSelectClause, 'join_clause' => &$sJoinClause, 'where_clause' => &$sWhereClause, 'order_clause' => &$sOrderClause, 'limit_clause' => &$sLimitClause));
+                break;
+
+            case 'all_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "`";
+                $sOrderClause .=  "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ADDED'] . "` ASC";
+                break;
+
+            case 'all':
+                $sOrderClause .=  "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ADDED'] . "` ASC";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_ENTRIES'] . "` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+		return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getEntriesNumByContext ($iProfileId)
+    {
+        $sQuery = $this->prepare ("SELECT COUNT(*) FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` WHERE `" . $this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO'] . "` = ?", - $iProfileId);
+        return $this->getOne($sQuery);
+    }
+
+    public function getEntriesByAuthor ($iProfileId)
+    {
+        $sQuery = $this->prepare ("SELECT * FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` WHERE `" . $this->_oConfig->CNF['FIELD_AUTHOR'] . "` = ? ORDER BY `" . $this->_oConfig->CNF['FIELD_ADDED'] . "` DESC", $iProfileId);
+        return $this->getAll($sQuery);
+    }
+
+    public function getEntriesNumByAuthor ($iProfileId)
+    {
+        $sQuery = $this->prepare ("SELECT COUNT(*) FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` WHERE `" . $this->_oConfig->CNF['FIELD_AUTHOR'] . "` = ?", $iProfileId);
+        return $this->getOne($sQuery);
+    }
+    
+    public function updateEntriesBy($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_ENTRIES']) || empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        return $this->query("UPDATE `" . $CNF['TABLE_ENTRIES'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
+
+    public function updateStatusAdmin($iContentId, $isActive)
+    {
+        $CNF = $this->_oConfig->CNF;
+        if (!isset($CNF['FIELD_STATUS_ADMIN']))
+            return false;
+        
+        $sQuery = $this->prepare("UPDATE `" . $CNF['TABLE_ENTRIES'] . "` SET `" . $CNF['FIELD_STATUS_ADMIN'] . "` = ? WHERE `" . $CNF['FIELD_ID'] . "` = ?", $isActive ? 'active' : 'hidden', $iContentId);
+        return $this->query($sQuery);
+    }
+    
+    public function publishEntries()
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        $aEntries = $this->getAll("SELECT `id`, `" . $CNF['FIELD_PUBLISHED'] . "`, FROM_UNIXTIME(`" . $CNF['FIELD_PUBLISHED'] . "`)  FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `" . $CNF['FIELD_PUBLISHED'] . "` > `" . $CNF['FIELD_ADDED'] . "` AND `" . $CNF['FIELD_STATUS'] . "` = 'awaiting'");
+        if(empty($aEntries) || !is_array($aEntries))
+            return false;
+
+        $iNow = time();
+        $aResult = array();
+        foreach($aEntries as $aEntry)
+            if($aEntry[$CNF['FIELD_PUBLISHED']] <= $iNow) 
+                $aResult[] = $aEntry[$CNF['FIELD_ID']];
+
+        $sSet = "`" . $CNF['FIELD_ADDED'] . "`=`" . $CNF['FIELD_PUBLISHED'] . "`, `" . $CNF['FIELD_STATUS'] . "` = 'active'";
+        if(isset($CNF['FIELD_CHANGED']))
+            $sSet .= ", `" . $CNF['FIELD_CHANGED'] . "`=`" . $CNF['FIELD_PUBLISHED'] . "`";
+
+        return count($aResult) == (int)$this->query("UPDATE `" . $CNF['TABLE_ENTRIES'] . "` SET " . $sSet . " WHERE `id` IN (" . $this->implode_escape($aResult) . ")") ? $aResult : false;
+    }
+
+    public function alterFulltextIndex ()
+    {
+        $CNF = $this->_oConfig->CNF;        
+
+        $bFulltextIndex = false;
+        $aIndexes = $this->getAll("SHOW INDEXES FROM `" . $CNF['TABLE_ENTRIES'] . "`");
+
+        foreach ($aIndexes as $r) {
+            if ($CNF['TABLE_ENTRIES_FULLTEXT'] == $r['Key_name']) {
+                $bFulltextIndex = true;
+                break;
+            }
+        }
+
+        if ($bFulltextIndex)
+            $this->query("ALTER TABLE `" . $CNF['TABLE_ENTRIES'] . "` DROP INDEX `" . $CNF['TABLE_ENTRIES_FULLTEXT'] . "`");
+
+        $sFields = getParam($CNF['PARAM_SEARCHABLE_FIELDS']);
+        if (!$sFields || !($aFields = explode(',', $sFields)))
+            return true;
+
+        $sFields = '';
+        foreach ($aFields as $s)
+            $sFields .= "`$s`,";
+
+        return $this->query("ALTER TABLE `" . $CNF['TABLE_ENTRIES'] . "` ADD FULLTEXT `" . $CNF['TABLE_ENTRIES_FULLTEXT'] . "` (" . trim($sFields, ', ') . ")");
+    }
+    
+    public function deleteNestedById ($iNestedId, $sTableKey, $sTableName)
+	{
+		return $this->query("DELETE FROM `" . $sTableName . "` WHERE `" . $sTableKey . "` = :content_id", array('content_id' => $iNestedId));
+	}
+   
+    function getNestedBy($aParams = array(), $sTableName)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array()));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`" . $sTableName . "`.*";
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['id'] = (int)$aParams['id'];
+
+                $sWhereClause .= " AND `" . $sTableName . "`.`" . $aParams['key_name'] . "` = :id";
+                break;
+                
+            case 'content_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][2]['id'] = (int)$aParams['id'];
+                $aMethod['params'][1] = $aParams['key_name'];
+                $sWhereClause .= " AND `" . $sTableName . "`.`content_id` = :id";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $sTableName . "` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+		return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+    
+    protected function _getEntriesBySearchIds($aParams, &$aMethod, &$sSelectClause, &$sJoinClause, &$sWhereClause, &$sOrderClause, &$sLimitClause)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aMethod['name'] = 'getColumn';
+
+        $sSelectClause = " DISTINCT `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "`";
+
+        if (!empty($aParams['start']) && !empty($aParams['per_page']))
+            $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+        elseif (!empty($aParams['per_page']))
+            $sLimitClause = $this->prepareAsString("?", $aParams['per_page']);
+
+        $sWhereConditions = "1";
+        foreach($aParams['search_params'] as $sSearchParam => $aSearchParam) {
+            if(empty($aSearchParam['operator']))
+                continue;
+
+            $sSearchValue = "";
+            switch ($aSearchParam['operator']) {
+                case 'like':
+                    if(is_array($aSearchParam['value'])) {
+                        $sSubCondition = "0";
+                        foreach($aSearchParam['value'] as $sValue)
+                            if(!empty($sValue))
+                                $sSubCondition .= " OR `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "` LIKE " . $this->_getEbsiLike($sValue);
+
+                        if($sSubCondition != "0")
+                            $sWhereConditions .= " AND (" . $sSubCondition . ")";
+                    }
+                    else
+                        $sSearchValue = " LIKE " . $this->_getEbsiLike($aSearchParam['value']);
+                    break;
+
+                case 'in':
+                    $sSearchValue = " IN (" . $this->implode_escape($aSearchParam['value']) . ")";
+                    break;
+
+                case 'not in':
+                    $sSearchValue = " NOT IN (" . $this->implode_escape($aSearchParam['value']) . ")";
+                    break;	
+                    
+                case 'and':
+                    $iResult = 0;
+                    if (is_array($aSearchParam['value']))
+                        foreach ($aSearchParam['value'] as $iValue)
+                            $iResult |= pow (2, $iValue - 1);
+                    else 
+                        $iResult = (int)$aSearchParam['value'];
+
+                    $sSearchValue = " & " . $iResult . "";
+                    break;
+
+                case 'locate':
+                    if(!isset($CNF['OBJECT_METATAGS']))
+                        break;
+                    if ($aSearchParam['type'] == 'location_radius'){
+                        if ($aSearchParam['value']['string'] != ''){
+                            list($fLatitude, $fLongitude, $sCountry, $sState, $sCity, $sZip, $sStreet, $sStreetNumber, $iRadius) = $aSearchParam['value']['array'];
+                            $aBounds = bx_get_location_bounds_latlng($fLatitude, $fLongitude, $iRadius);   
+                            $aSql = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])->locationsGetAsSQLPart($CNF['TABLE_ENTRIES'], $CNF['FIELD_ID'], '', '', '', '', $aBounds);
+                        }
+                    }
+                    else{
+                        list($fLatitude, $fLongitude, $sCountry, $sState, $sCity, $sZip) = $aSearchParam['value']['array'];
+                        $aSql = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])->locationsGetAsSQLPart($CNF['TABLE_ENTRIES'], $CNF['FIELD_ID'], $sCountry, $sState, $sCity, $sZip);
+                    }
+                    if(!empty($aSql['where'])) {
+                        $sWhereConditions .= $aSql['where'];
+
+                        if(!empty($aSql['join']))
+                            $sJoinClause .= $aSql['join'];
+                    }
+                    break;
+                    
+                case 'between':
+                    if(!is_array($aSearchParam['value']) || count($aSearchParam['value']) != 2) 
+                        break;
+
+                    list($mixedMin, $mixedMax) = $aSearchParam['value'];
+
+                    if(!empty($mixedMin)) {
+                        $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "` >= :" . $sSearchParam . "_from";
+                        
+                        $aMethod['params'][1][$sSearchParam . "_from"] = $mixedMin; 
+                    }
+
+                    if(!empty($mixedMax)) {
+                        $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "` <= :" . $sSearchParam . "_to";
+
+                        $aMethod['params'][1][$sSearchParam . "_to"] = $mixedMax; 
+                    }
+                    break;
+
+                default:
+                    $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam;
+                    $aMethod['params'][1][$sSearchParam] = $aSearchParam['value'];
+            }
+
+            if(!empty($sSearchValue))
+                $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "`" . $sSearchValue;
+        }
+
+        $sWhereClause .= " AND (" . $sWhereConditions . ")"; 
+
+        $this->_getEntriesBySearchIdsOrder($aParams, $sOrderClause);
+    }
+
+    protected function _getEntriesBySearchIdsOrder($aParams, &$sOrderClause)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParams['search_params']['order']) || !is_array($aParams['search_params']['order'])) 
+            $aParams['search_params']['order'] = array(
+                array('table' => $CNF['TABLE_ENTRIES'], 'field' => $CNF['FIELD_ADDED'], 'direction' => 'DESC')
+            );
+
+        $aOrders = array();
+        foreach($aParams['search_params']['order'] as $aOrder) 
+            $aOrders[] = "`" . (isset($aOrder['table']) ? $aOrder['table'] : $CNF['TABLE_ENTRIES']) . "`.`" . (!empty($aOrder['field']) ? $aOrder['field'] : $CNF['FIELD_ADDED']) . "` " . (!empty($aOrder['direction']) ? strtoupper($aOrder['direction']) : 'DESC');
+
+        $sOrderClause .= implode(', ', $aOrders);
+    }
+
+    protected function _getEbsiLike($sValue)
+    {
+        return $this->escape("%" . preg_replace('/\s+/', '%', $sValue) . "%");
+    }
+}
+
+/** @} */

+ 939 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php

@@ -0,0 +1,939 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Create/edit entry form
+ */
+class BxBaseModGeneralFormEntry extends BxTemplFormView
+{
+    protected static $_isCssJsGeneralModuleAdded = false;
+        
+    protected $MODULE;
+
+    protected $_oModule;
+
+    protected $_aMetatagsFieldsWithKeywords = array();
+    protected $_oMetatagsObject = null;
+    protected $_oMetatagsContentId = 0;
+
+    protected $_sGhostTemplate = 'form_ghost_template.html';
+    
+    protected $_aTrackFieldsChanges;
+    
+    protected $_iContentId;
+
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        $this->_aTrackFieldsChanges = array();
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if (isset($CNF['FIELD_ADDED']) && isset($this->aInputs[$CNF['FIELD_ADDED']])) {
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($CNF['FIELD_CHANGED']) && isset($this->aInputs[$CNF['FIELD_CHANGED']])) {
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($CNF['FIELD_TEXT']) && isset($this->aInputs[$CNF['FIELD_TEXT']]) && isset($CNF['FIELD_TEXT_ID'])) {
+            $this->aInputs[$CNF['FIELD_TEXT']]['attrs'] = array_merge (
+                array ('id' => $CNF['FIELD_TEXT_ID']),
+                is_array($this->aInputs[$CNF['FIELD_TEXT']]['attrs']) ? $this->aInputs[$CNF['FIELD_TEXT']]['attrs'] : array ()
+            );
+        }
+
+        if (isset($CNF['FIELD_LABELS']) && isset($this->aInputs[$CNF['FIELD_LABELS']]) && !empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) && $oMetatags->keywordsIsEnabled()) {
+            $this->aInputs[$CNF['FIELD_LABELS']]['meta_object'] = $CNF['OBJECT_METATAGS'];
+        }
+
+        if (isset($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']])) {
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['manual_input'] = true;
+        }
+
+        if (isset($CNF['FIELD_PHOTO']) && isset($this->aInputs[$CNF['FIELD_PHOTO']])) {
+            $this->aInputs[$CNF['FIELD_PHOTO']]['storage_object'] = $CNF['OBJECT_STORAGE'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['uploaders'] = !empty($this->aInputs[$CNF['FIELD_PHOTO']]['value']) ? unserialize($this->aInputs[$CNF['FIELD_PHOTO']]['value']) : $CNF['OBJECT_UPLOADERS'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['images_transcoder'] = $CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['storage_private'] = 0;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['multiple'] = true;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'] = 0;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['ghost_template'] = '';
+        }
+
+        $aPrivacyFields = $this->_getPrivacyFields();
+        foreach($aPrivacyFields as $sField => $sObject)
+            $this->_preparePrivacyField($sField, $sObject);
+        
+        foreach($this->aInputs as $sKey => $aInput) {
+            if ($aInput['type'] == 'nested_form'){
+                $this->genGhostTemplateForInputNestedForm ($this->aInputs[$sKey]);
+            }
+        }
+    }
+
+    function getCode($bDynamicMode = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if(!$bDynamicMode && bx_is_dynamic_request())
+            $bDynamicMode = true;
+       
+        $sResult = parent::getCode($bDynamicMode);
+
+        $aPrivacyFields = $this->_getPrivacyFields();
+        foreach($aPrivacyFields as $sField => $sObject)
+            $this->_addCssJsPrivacyField($sField, $sObject, $bDynamicMode);
+
+        if(isset($CNF['PARAM_MULTICAT_ENABLED']) && $CNF['PARAM_MULTICAT_ENABLED'] === true) {
+            $sInclude2 = '';
+            $sInclude2 .= $this->_oModule->_oTemplate->addJs(array('BxDolCategories.js'), $bDynamicMode);
+            $sInclude2 .= $this->_oModule->_oTemplate->addCss(array('categories.css'), $bDynamicMode);
+
+            $sResult .= ($bDynamicMode ? $sInclude2 : '') . $this->_oModule->_oTemplate->getJsCode('categories');
+        }
+        
+        return $sResult;
+    }
+    
+    /**
+     * Nested forms processing
+     */
+    function genInputNestedForm (&$aInput, $sInfo = '', $sError = '')
+    {
+        $sUniqId = genRndPwd (8, false);
+        $aNestedForms = array();
+        if (!$this->isSubmitted()){
+            $aNestedValues = $this->_oModule->_oDb->getNestedBy(array('type' => 'content_id', 'id' => $this->_iContentId, 'key_name' => $aInput['ghost_template']['params']['db']['key']), $aInput['ghost_template']['params']['db']['table']);
+            foreach($aNestedValues as $aNestedValue) {
+                $aNestedValuesRv = array();
+                $oForm = $this->getNestedFormObject($aInput);  
+                
+                foreach($aNestedValue as $aNestedKey => $aNestedItem) {
+                    if ($oForm->aParams['db']['key'] == $aNestedKey)
+                        $aNestedValuesRv[$aNestedKey . '[]'] = $aNestedItem;
+                    else
+                        $aNestedValuesRv[$aNestedKey] = $aNestedItem;
+                }
+                
+                $oForm->initChecker($aNestedValuesRv);
+                array_push(
+				    $aNestedForms,
+				    array(
+					    'key_value' => $aNestedValuesRv[$oForm->aParams['db']['key'] . '[]'],
+                        'key_name' => $aInput['name'],
+					    'form_code' => $oForm->genRows(),
+                        'js_instance_name' => 'oBxNestedForm_' . $sUniqId,
+                        'nested_type' => $aInput['name'],
+				    )
+			    );
+            }
+        }
+        else{
+            $aInput['ghost_templates'] = $aInput['ghost_template'];
+            $this->genGhostTemplateForInputNestedForm ($aInput);
+            foreach($aInput['ghost_templates'] as $oForm) {
+                array_push(
+				    $aNestedForms,
+				    array(
+					    'key_value' => $oForm->aInputs[$aInput['name']]['value'],
+                        'key_name' => $aInput['name'],
+					    'form_code' => $oForm->genRows(),
+                        'js_instance_name' => 'oBxNestedForm_' . $sUniqId,
+                        'nested_type' => $aInput['name'],
+				    )
+			    );
+            }
+        }
+
+        return $this->oTemplate->parseHtmlByName('form_field_nested_form.html', array(
+            'bx_repeat:items' => $aNestedForms,
+            'info' => $sInfo,
+            'error' => $sError,
+            'uniq_id' => $sUniqId,
+            'js_instance_name' => 'oBxNestedForm_' . $sUniqId,
+            'options' => json_encode(array(
+                'uniq_id' => $sUniqId,
+                'js_instance_name' => 'oBxNestedForm_' . $sUniqId,
+                'template_ghost' => $this->genGhostTemplate($aInput),
+                'form_name' => $aInput['value'],
+                'action_uri' =>  $this->_oModule->_oConfig->getBaseUri(),
+                'nested_type' => $aInput['name'],
+            )),
+        ));
+    }
+    
+    function genNestedForm (&$aInput)
+    {
+        $sResult = '';
+        $aNestedValues = $this->_oModule->_oDb->getNestedBy(array('type' => 'content_id', 'id' => $this->_iContentId, 'key_name' => $aInput['ghost_template']['params']['db']['key']), $aInput['ghost_template']['params']['db']['table']);
+        foreach($aNestedValues as $aNestedValue) {
+            $sValue = '';
+			$aNestedValuesRv = array();
+            $oForm = $this->getNestedFormObject($aInput, true);
+            
+            foreach($aNestedValue as $aNestedKey => $aNestedItem) {
+                if ($oForm->aParams['db']['key'] == $aNestedKey)
+                    $aNestedValuesRv[$aNestedKey . '[]'] = $aNestedItem;
+                else
+                    $aNestedValuesRv[$aNestedKey] = $aNestedItem;
+            }
+              
+            $oForm->initChecker($aNestedValuesRv);
+            
+            if ($aInput['rateable']){
+                $sVote = '';
+				$sVoteBtn = '';
+                $iId = BxDolFormQuery::getFormField($this->id, $aInput['name'], $this->_iContentId,  $aNestedValuesRv[$oForm->aParams['db']['key'] . '[]']);
+                $oVote = BxDolVote::getObjectInstance($aInput['rateable'], $iId, true, BxDolTemplate::getInstance());
+                if ($oVote){
+					$sVote = $oVote->getCounter(array('show_counter_empty' => true, 'show_counter' => true, 'show_counter_style' => 'simple', 'dynamic_mode' => $this->_bDynamicMode));
+					$sVoteBtn = $oVote->getElementInline(array('show_counter_empty' => true, 'show_counter' => false, 'show_counter_style' => 'simple', 'dynamic_mode' => $this->_bDynamicMode));
+				}
+				$sValue = $this->oTemplate->parseHtmlByName('form_view_rateable_row.html', array(
+					'value' =>  $oForm->getCode(),
+					'rate' => $sVote,
+					'rate_btn' => $sVoteBtn
+				));
+            }
+			else{
+				$sValue =  $oForm->getCode();
+			}
+			$sResult .= $this->oTemplate->parseHtmlByName('form_view_nested_row.html', array(
+				'value' =>  $sValue
+			));
+        }
+        if ($sResult != ''){
+            return $this->oTemplate->parseHtmlByName('form_view_row.html', array(
+                'type' => $aInput['type'], 
+                'caption' => isset($aInput['caption']) ? bx_process_output($aInput['caption']) : '',
+                'value' => $sResult
+            ));
+        }
+    }
+    
+    protected function genGhostTemplateForInputNestedForm (&$aInput)
+    {
+        $oForm = $this->getNestedFormObject($aInput);  
+        $aInput['ghost_template'] = array ( 
+            'params' => array(
+                'nested_form_template' => 'form_field_nested_form_wrapper.html',
+                'db' => array(
+                    'table' => $oForm->aParams['db']['table'],
+                    'key' => $oForm->aParams['db']['key'],
+                    'submit_name' =>  $oForm->aParams['db']['key'],
+                ),
+            ),
+            'inputs' => $oForm->aInputs
+        );
+    }
+    
+    function getNestedFormObject (&$aInput, $bViewMode = false)
+    {
+        $oForm = BxDolForm::getObjectInstance($aInput['value'], $aInput['value'] . ($bViewMode ? '_view' : ''));
+        foreach($oForm->aInputs as $sKey => $aInput2) {
+            $sName = $oForm->aInputs[$sKey]['name'];
+            if (strpos($sName,'[]') === false)
+                $oForm->aInputs[$sKey]['name'] = $sName . '[]';
+        }
+        return $oForm;
+    }
+
+    /**
+     * Add field(s) which will be tracked during content update. 
+     */
+    public function addTrackFields($mixedFields, $mixedContent)
+    {
+        if(empty($mixedContent) || empty($mixedFields))
+            return;
+
+        if(!is_array($mixedContent)) {
+            $mixedContent = $this->_oModule->_oDb->getContentInfoById((int)$mixedContent);
+            if(empty($mixedContent) || !is_array($mixedContent))
+                return;
+        }
+
+        if(!is_array($mixedFields))
+            $mixedFields = array($mixedFields);
+
+        foreach($mixedFields as $sField) {
+            if(!isset($mixedContent[$sField]))
+                continue;
+
+            $this->_aTrackFieldsChanges[$sField] = array(
+                'old' => $mixedContent[$sField],
+            );
+        }
+    }
+
+    /**
+     * Checks if the field was changed.
+     */
+    public function isTrackFieldChanged($sField, $bReturnValues = false)
+    {
+        if(!isset($this->_aTrackFieldsChanges[$sField]) || $this->_aTrackFieldsChanges[$sField] === false)
+            return false;
+
+        return $bReturnValues ? $this->_aTrackFieldsChanges[$sField] : true;
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        $bValues = $aValues && !empty($aValues['id']);
+        
+        $this->_iContentId = isset($aValues['id']) ? $aValues['id'] : false;
+        
+        if (!empty($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]) && isset($aValues[$CNF['FIELD_ID']]) && !empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) && $oMetatags->locationsIsEnabled())
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['value'] = $oMetatags->locationsString($aValues[$CNF['FIELD_ID']], false);
+
+        if (isset($CNF['FIELD_PHOTO']) && isset($this->aInputs[$CNF['FIELD_PHOTO']])) {
+
+            $aContentInfo = false;
+            if ($aValues && !empty($aValues['id'])) {
+                $aContentInfo = $this->_oModule->_oDb->getContentInfoById ($aValues['id']);
+                $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'] = $aValues['id'];
+            }
+
+            $this->aInputs[$CNF['FIELD_PHOTO']]['ghost_template'] = $this->_oModule->_oTemplate->parseHtmlByName($this->_sGhostTemplate, $this->_getPhotoGhostTmplVars($aContentInfo));
+        }
+
+        if (isset($CNF['FIELD_LABELS']) && isset($this->aInputs[$CNF['FIELD_LABELS']]) && !empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) && $oMetatags->keywordsIsEnabled() && !empty($aValues['id'])) {
+            $this->aInputs[$CNF['FIELD_LABELS']]['content_id'] = $aValues['id'];
+            if(($aLabels = $oMetatags->keywordsGet($aValues['id'])))
+                $this->aInputs[$CNF['FIELD_LABELS']]['value'] = array_intersect($aLabels, BxDolLabel::getInstance()->getLabels(array('type' => 'values')));
+        }
+
+        if (isset($CNF['FIELD_ANONYMOUS']) && isset($this->aInputs[$CNF['FIELD_ANONYMOUS']]) && isset($CNF['FIELD_AUTHOR']) && isset($aValues[$CNF['FIELD_AUTHOR']])) {
+            $this->aInputs[$CNF['FIELD_ANONYMOUS']]['checked'] = $aValues[$CNF['FIELD_AUTHOR']] < 0;
+        }
+        
+        if (isset($CNF['FIELD_MULTICAT']) && isset($this->aInputs[$CNF['FIELD_MULTICAT']]) && $CNF['PARAM_MULTICAT_ENABLED']) {
+            if ($bValues)
+                $this->aInputs[$CNF['FIELD_MULTICAT']]['content_id'] = $aValues['id'];
+        }
+
+        $aPrivacyFields = $this->_getPrivacyFields();
+        foreach($aPrivacyFields as $sField => $sObject)
+            $this->_preloadPrivacyField($sField, $sObject, $aValues);
+
+        parent::initChecker ($aValues, $aSpecificValues);
+    }
+    
+    public function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($CNF['FIELD_AUTHOR']) && empty($aValsToAdd[$CNF['FIELD_AUTHOR']]))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = (isset($CNF['FIELD_ANONYMOUS']) && isset($this->aInputs[$CNF['FIELD_ANONYMOUS']]) && $this->getCleanValue($CNF['FIELD_ANONYMOUS']) ? -1 : 1) * bx_get_logged_profile_id ();
+
+        if (isset($CNF['FIELD_ADDED']) && empty($aValsToAdd[$CNF['FIELD_ADDED']]) && empty($this->getCleanValue($CNF['FIELD_ADDED'])))
+            $aValsToAdd[$CNF['FIELD_ADDED']] = time();
+
+        if (isset($CNF['FIELD_CHANGED']) && empty($aValsToAdd[$CNF['FIELD_CHANGED']]) && empty($this->getCleanValue($CNF['FIELD_CHANGED'])))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb() && isset($CNF['FIELD_THUMB'])) {
+            $aThumb = isset($_POST[$CNF['FIELD_THUMB']]) ? bx_process_input ($_POST[$CNF['FIELD_THUMB']], BX_DATA_INT) : false;
+            $aValsToAdd[$CNF['FIELD_THUMB']] = 0;
+            if (!empty($aThumb) && is_array($aThumb) && ($iFileThumb = array_pop($aThumb)))
+                $aValsToAdd[$CNF['FIELD_THUMB']] = $iFileThumb;
+        }
+
+        if(!empty($CNF['OBJECT_METATAGS']))
+            $this->_processMetas($aValsToAdd);
+
+        if(isset($CNF['FIELD_STATUS']) && !empty($CNF['FIELDS_DELAYED_PROCESSING']) && !empty($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4'])) {
+            $oTranscoder = BxDolTranscoder::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']);
+
+            $aFields = $CNF['FIELDS_DELAYED_PROCESSING'];
+            if(is_string($aFields))
+                $aFields = explode(',', $aFields);
+
+            foreach($aFields as $sField) {
+                $mixedFieldValues = $this->getCleanValue($sField);
+                if(empty($mixedFieldValues)) 
+                    continue;
+
+                $oStorage = BxDolStorage::getObjectInstance($this->aInputs[$sField]['storage_object']);
+                if(!$oStorage)
+                    continue;
+
+                if(!is_array($mixedFieldValues))
+                    $mixedFieldValues = array($mixedFieldValues);
+
+                foreach($mixedFieldValues as $mixedFieldValue) {
+                    $aInfo = $oStorage->getFile((int)$mixedFieldValue);
+                    if(empty($aInfo) || !is_array($aInfo))
+                        continue;
+
+                    if(strncmp($aInfo['mime_type'], 'video/', 6) != 0) 
+                        continue;
+
+                    if($oTranscoder->isFileReady((int)$mixedFieldValue))
+                        continue;
+
+                    $aValsToAdd[$CNF['FIELD_STATUS']] = 'awaiting';
+                    break 2;
+                }
+            }
+        }
+
+        $bMulticatEnabled = $this->_isMulticatEnabled();
+        
+        if ($bMulticatEnabled)
+            $this->processMulticatBefore($CNF['FIELD_MULTICAT'], $aValsToAdd);
+        
+        $iContentId = parent::insert ($aValsToAdd, $isIgnore);
+        
+        if(!empty($iContentId)){
+            foreach($this->aInputs as $aInput) {
+                if ($aInput['type'] == 'nested_form'){
+			        if (is_array($aInput['ghost_template']) && !isset($aInput['ghost_template']['inputs'])) {
+				        foreach ($aInput['ghost_template'] as $oFormNested) {
+					        $iNestedContentId = $oFormNested->insert(array('content_id' => $iContentId));
+                            if ($aInput['rateable']){
+                                BxDolFormQuery::addFormField($this->id, $aInput['name'], $iContentId, $aValsToAdd[$CNF['FIELD_AUTHOR']], $this->_oModule->getName(), $iNestedContentId);
+                            }
+				        }
+			        }
+                }
+		    }
+            
+            if ($bMulticatEnabled)
+                $this->processMulticatAfter($CNF['FIELD_MULTICAT'], $iContentId);
+        }
+        
+        
+        foreach($this->aInputs as $aInput) {
+            if (isset($aInput['rateable']) && $aInput['rateable'] && $aInput['type'] != 'nested_form'){
+                BxDolFormQuery::addFormField($this->id, $aInput['name'], $iContentId, $aValsToAdd[$CNF['FIELD_AUTHOR']], $this->_oModule->getName());
+            }
+        }
+        
+        return $iContentId;
+    }
+
+    function update ($iContentId, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+
+        if(isset($CNF['FIELD_AUTHOR']) && isset($CNF['FIELD_ANONYMOUS']) && isset($this->aInputs[$CNF['FIELD_ANONYMOUS']]))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = ($this->getCleanValue($CNF['FIELD_ANONYMOUS']) ? -1 : 1) * abs($aContentInfo[$CNF['FIELD_AUTHOR']]);
+
+        if(isset($CNF['FIELD_CHANGED']) && empty($aValsToAdd[$CNF['FIELD_CHANGED']]) && empty($this->getCleanValue($CNF['FIELD_CHANGED'])))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+
+        if(CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb($iContentId) && isset($CNF['FIELD_THUMB'])) {
+            $aThumb = bx_process_input (bx_get($CNF['FIELD_THUMB']), BX_DATA_INT);
+            $aValsToAdd[$CNF['FIELD_THUMB']] = 0;
+            if (!empty($aThumb) && is_array($aThumb) && ($iFileThumb = array_pop($aThumb)))
+                $aValsToAdd[$CNF['FIELD_THUMB']] = $iFileThumb;
+        }
+
+        if(!empty($CNF['OBJECT_METATAGS']))
+            $this->_processMetas($aValsToAdd);
+
+        if(isset($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]) && $aContentInfo[$CNF['FIELD_STATUS']] == 'failed')
+            $aValsToAdd[$CNF['FIELD_STATUS']] = 'active';
+
+        $bMulticatEnabled = $this->_isMulticatEnabled();
+        if ($bMulticatEnabled)
+            $this->processMulticatBefore($CNF['FIELD_MULTICAT'], $aValsToAdd);
+        
+        $mixedResult = parent::update($iContentId, $aValsToAdd, $aTrackTextFieldsChanges);
+        
+        foreach($this->aInputs as $aInput) {
+            if ($aInput['type'] == 'nested_form'){
+                if (is_array($aInput['ghost_template']) && !isset($aInput['ghost_template']['inputs'])) {
+                    foreach ($aInput['ghost_template'] as $oFormNested) {
+                        $iNestedContentId = $oFormNested->aInputs[$aInput['name']]['value'];
+                        if (empty($iNestedContentId)){
+                            $iNestedContentId = $oFormNested->insert(array('content_id' => $iContentId));
+                            if ($aInput['rateable']){
+                                BxDolFormQuery::addFormField($this->id, $aInput['name'], $iContentId, $aContentInfo[$CNF['FIELD_AUTHOR']], $this->_oModule->getName(), $iNestedContentId);
+                            }
+                        }
+                        else{
+                            $oFormNested->update($iNestedContentId, array('content_id' => $iContentId));
+                        }
+                    }
+                }
+            }
+        }
+        
+        if ($bMulticatEnabled)
+            $this->processMulticatAfter($CNF['FIELD_MULTICAT'], $iContentId);
+        
+        if($mixedResult !== false)
+            $this->_processTrackFields($iContentId);
+
+        return $mixedResult;
+    }
+
+    protected function _associalFileWithContent($oStorage, $iFileId, $iProfileId, $iContentId, $sPictureField = '')
+    {
+        $oStorage->updateGhostsContentId ($iFileId, $iProfileId, $iContentId, $this->_isAdmin($iContentId));
+    }
+
+    protected function _getPhotoGhostTmplVars($aContentInfo = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	return array (
+			'name' => $this->aInputs[$CNF['FIELD_PHOTO']]['name'],
+			'content_id' => (int)$this->aInputs[$CNF['FIELD_PHOTO']]['content_id'],
+			'editor_id' => isset($CNF['FIELD_TEXT_ID']) ? $CNF['FIELD_TEXT_ID'] : '',
+            'thumb_id' => isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) ? $aContentInfo[$CNF['FIELD_THUMB']] : 0,
+            'name_thumb' => isset($CNF['FIELD_THUMB']) ? $CNF['FIELD_THUMB'] : '',
+			'bx_if:set_thumb' => array (
+				'condition' => CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb($this->aInputs[$CNF['FIELD_PHOTO']]['content_id']),
+				'content' => array (
+					'name_thumb' => isset($CNF['FIELD_THUMB']) ? $CNF['FIELD_THUMB'] : '',
+    				'txt_pict_use_as_thumb' => _t(!empty($CNF['T']['txt_pict_use_as_thumb']) ? $CNF['T']['txt_pict_use_as_thumb'] : '_sys_txt_form_entry_input_picture_use_as_thumb')
+				),
+			),
+		);
+    }
+    
+    function delete ($iContentId, $aContentInfo = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // delete associated files
+
+        if (!empty($CNF['OBJECT_STORAGE'])) {
+            $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+            if ($oStorage)
+                $oStorage->queueFilesForDeletionFromGhosts($aContentInfo[$CNF['FIELD_AUTHOR']], $iContentId);
+        }
+
+        // delete associated objects data
+
+        if (!empty($CNF['OBJECT_VIEWS'])) {
+            $o = BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_VOTES'])) {
+            $o = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_SCORES'])) {
+            $o = BxDolScore::getObjectInstance($CNF['OBJECT_SCORES'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_COMMENTS'])) {
+            $o = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $o = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($o) $o->onDeleteContent($iContentId);
+        }
+
+        // delete db record
+        
+        BxDolFormQuery::removeFormField($this->id, $iContentId);
+
+        return parent::delete($iContentId);
+    }
+
+    protected function _isAdmin ($iContentId = 0)
+    {
+        return $this->_oModule->_isModerator();
+    }
+
+    public function processFiles ($sFieldFile, $iContentId = 0, $isAssociateWithContent = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!isset($this->aInputs[$sFieldFile]))
+            return true;
+
+        $mixedFileIds = $this->getCleanValue($sFieldFile);
+        if(!$mixedFileIds)
+            return true;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->aInputs[$sFieldFile]['storage_object']);
+        if (!$oStorage)
+            return false;
+
+        $iProfileId = $this->getContentOwnerProfileId($iContentId);
+
+        $aGhostFiles = $oStorage->getGhosts ($iProfileId, $isAssociateWithContent ? 0 : $iContentId, true, $this->_isAdmin($iContentId));
+        if (!$aGhostFiles)
+            return true;
+
+        foreach ($aGhostFiles as $aFile) {
+            if (is_array($mixedFileIds) && !in_array($aFile['id'], $mixedFileIds))
+                continue;
+
+            if ($aFile['private'])
+                $oStorage->setFilePrivate ($aFile['id'], 1);
+
+            if ($iContentId)
+                $this->_associalFileWithContent($oStorage, $aFile['id'], $iProfileId, $iContentId, $sFieldFile);
+        }
+
+        return true;
+    }
+
+    function _deleteFile ($iFileId, $sStorage = '')
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$iFileId)
+            return true;
+
+		$sStorage = !empty($sStorage) ? $sStorage : $CNF['OBJECT_STORAGE'];
+		$oStorage = BxDolStorage::getObjectInstance($sStorage);
+        if (!$oStorage)
+            return false;
+
+        if (!$oStorage->getFile($iFileId))
+            return true;
+
+        $iProfileId = bx_get_logged_profile_id();
+        return $oStorage->deleteFile($iFileId, $iProfileId);
+    }
+
+    function addCssJs ()
+    {
+        if ((!isset($this->aParams['view_mode']) || !$this->aParams['view_mode']) && !self::$_isCssJsGeneralModuleAdded) {
+            $this->_oModule->_oTemplate->addCss('forms.css');
+            $this->_oModule->_oTemplate->addJs('modules/base/general/js/|forms.js');
+            self::$_isCssJsGeneralModuleAdded = true;
+        }
+
+        return parent::addCssJs ();
+    }
+    
+    function genViewRowValue(&$aInput)
+    {
+        $s = parent::genViewRowValue($aInput);
+
+        if ($this->_oMetatagsObject && in_array($aInput['name'], $this->_aMetatagsFieldsWithKeywords) && $s)
+            $s = $this->_oMetatagsObject->metaParse($this->_oMetatagsContentId, $s);
+
+        return $s;
+    }
+    
+    function genViewRowWrapped(&$aInput)
+    {
+        $sResult =  parent::genViewRowWrapped($aInput);
+        if (!$aInput['rateable']){
+            return $sResult;
+        }
+        
+        $sValue = $this->genViewRowValue($aInput);
+        if (null === $sValue)
+            return '';
+        
+        // process rateable fields
+        $iId = BxDolFormQuery::getFormField($this->id, $aInput['name'], $this->_iContentId);
+        $sVote = '';
+        $oVote = BxDolVote::getObjectInstance($aInput['rateable'], $iId, true, BxDolTemplate::getInstance());
+        if ($oVote){
+            $sVote = $oVote->getCounter(array('show_counter_empty' => true, 'show_counter' => true, 'show_counter_style' => 'simple', 'dynamic_mode' => $this->_bDynamicMode));
+			$sVoteBtn = $oVote->getElementInline(array('show_counter_empty' => true, 'show_counter' => false, 'show_counter_style' => 'simple', 'dynamic_mode' => $this->_bDynamicMode));
+			return $this->oTemplate->parseHtmlByName('form_view_rateable_row.html', array(
+				'value' => $sResult,
+				'rate' => $sVote,
+				'rate_btn' => $sVoteBtn
+			));
+        }
+		return $sResult;
+    }
+
+    function setMetatagsKeywordsData($iId, $a, $o)
+    {
+        $this->_oMetatagsContentId = $iId;
+        $this->_aMetatagsFieldsWithKeywords = $a;
+        $this->_oMetatagsObject = $o;
+    }
+
+    function getContentOwnerProfileId ($iContentId)
+    {
+        return $this->_oModule->serviceGetContentOwnerProfileId($iContentId);
+    }
+
+    protected function _processTrackFields($mixedContent)
+    {
+        if(empty($mixedContent) || empty($this->_aTrackFieldsChanges))
+            return;
+
+        if(!is_array($mixedContent)) {
+            $mixedContent = $this->_oModule->_oDb->getContentInfoById((int)$mixedContent);
+            if(empty($mixedContent) || !is_array($mixedContent))
+                return;
+        }
+
+        foreach($this->_aTrackFieldsChanges as $sField => $aValues)
+            if($mixedContent[$sField] == $aValues['old'])
+                $this->_aTrackFieldsChanges[$sField] = false;
+            else
+                $this->_aTrackFieldsChanges[$sField]['new'] = $mixedContent[$sField];
+    }
+
+    protected function _processMetas(&$aValsToAdd)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sKey = 'FIELD_LABELS';
+        if(isset($CNF[$sKey]) && isset($this->aInputs[$CNF[$sKey]]) && empty($aValsToAdd[$CNF[$sKey]])) {
+            $aLabels = $this->getCleanValue($CNF[$sKey]);
+            if(!empty($aLabels) && is_array($aLabels))
+                $aValsToAdd[$CNF[$sKey]] = serialize($aLabels);
+            else
+                $aValsToAdd[$CNF[$sKey]] = '';
+        }
+
+        $sKey1 = 'FIELD_LOCATION';
+        $sKey2 = 'FIELD_LOCATION_PREFIX';
+        if(isset($CNF[$sKey1]) && isset($CNF[$sKey2]) && isset($this->aInputs[$CNF[$sKey1]]) && empty($aValsToAdd[$CNF[$sKey1]])) {               
+            $aLocation = BxDolMetatags::locationsRetrieveFromForm($CNF[$sKey2], $this);
+            if(!empty($aLocation) && is_array($aLocation))
+                $aValsToAdd[$CNF[$sKey1]] = serialize(BxDolMetatags::locationsParseComponents($aLocation));
+        }
+    }
+
+    protected function _getPrivacyFields($aKeysF2O = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($CNF['PRIVACY_FIELD_TO_OBJECT']) && is_array($CNF['PRIVACY_FIELD_TO_OBJECT']))
+            return $CNF['PRIVACY_FIELD_TO_OBJECT'];
+
+        if(empty($aKeysF2O))
+            $aKeysF2O = array('FIELD_ALLOW_VIEW_TO' => 'OBJECT_PRIVACY_VIEW');
+
+        $aResult = array();
+        foreach($aKeysF2O as $sKeyField => $sKeyObject) {
+            if(!isset($CNF[$sKeyField]) || !isset($this->aInputs[$CNF[$sKeyField]]) || !isset($CNF[$sKeyObject]))
+                continue;
+
+            $aResult[$CNF[$sKeyField]] = $CNF[$sKeyObject];
+        }
+
+        return $aResult;
+    }
+
+    function _addCssJsPrivacyField($sField, $sPrivacyObject, $bDynamicMode = false)
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sPrivacyObject);
+        if(!$oPrivacy) 
+            return;
+
+        $this->aInputs[$sField]['content'] = $oPrivacy->addCssJs($bDynamicMode) . $this->aInputs[$sField]['content'];
+    }
+
+    protected function _preparePrivacyField($sField, $sPrivacyObject)
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sPrivacyObject);
+        if(!$oPrivacy) 
+            return;
+
+        $aSave = array('db' => array('pass' => 'Xss'));
+        array_walk($this->aInputs[$sField], function ($a, $k, $aSave) {
+            if (in_array($k, array('info', 'caption', 'value')))
+                $aSave[0][$k] = $a;
+        }, array(&$aSave));
+
+        $aGroupChooser = $oPrivacy->getGroupChooser($sPrivacyObject, 0, array(
+            'object' => $this->aParams['object'],
+            'display' => $this->aParams['display']
+        ));
+
+        $this->aInputs[$sField] = array_merge($this->aInputs[$sField], $aGroupChooser, $aSave);
+    }
+
+    protected function _preloadPrivacyField($sField, $sPrivacyObject, $aValues)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sPrivacyObject);
+        if(!$oPrivacy) 
+            return;
+
+        $iContentId = !empty($aValues[$CNF['FIELD_ID']]) ? (int)$aValues[$CNF['FIELD_ID']] : 0;
+        $iProfileId = !empty($iContentId) ? (int)$this->getContentOwnerProfileId($iContentId) : bx_get_logged_profile_id();
+        $iGroupId = !empty($aValues[$sField]) ? $aValues[$sField] : 0;
+
+        if(!isset($this->aInputs[$sField]['content']))
+            $this->aInputs[$sField]['content'] = '';
+
+        $this->aInputs[$sField]['content'] .= $oPrivacy->initGroupChooser($sPrivacyObject, $iProfileId, array(
+            'content_id' => $iContentId,
+            'group_id' => $iGroupId,
+            'html_ids' => array(
+                'form' => $this->getId()
+            )
+        ));
+    }
+    
+    /**
+     * 
+     * MultiCategories related methods. 
+     * 
+     */
+    
+    protected function processMulticatBefore($sFieldName, &$aValsToAdd){
+        if (isset($this->aInputs[$sFieldName])){
+            $this->aInputs[$sFieldName]['value'] = array_unique(array_filter($this->aInputs[$sFieldName]['value'], function($sTmp){
+               return trim($sTmp);
+            }));  
+            $aValsToAdd[$sFieldName] = implode(',', $this->aInputs[$sFieldName]['value']);
+        }
+    }
+    
+    protected function processMulticatAfter($sFieldName, $iContentId){
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $bAutoActivation = (isset($CNF['PARAM_MULTICAT_AUTO_ACTIVATION_FOR_CATEGORIES']) && getParam($CNF['PARAM_MULTICAT_AUTO_ACTIVATION_FOR_CATEGORIES']) == 'on') ? true : false;
+		$oCategories = BxDolCategories::getInstance();
+        if (isset($this->aInputs[$sFieldName])){
+            $oCategories->delete($this->_oModule->getName(), $iContentId);
+            foreach($this->aInputs[$sFieldName]['value'] as  $sValue) {
+                $oCategories->add($this->_oModule->getName(), bx_get_logged_profile_id(), $sValue, $iContentId, $bAutoActivation);
+            }
+        }
+    }
+    
+    protected function genCustomViewRowValueMulticat(&$aInput)
+    {
+		$oCategories = BxDolCategories::getInstance();
+        $aValues = $oCategories->getData(array('type' => 'by_module_and_object', 'module' => $this->_oModule->getName(), 'object_id' => (!empty($aInput['content_id']) ? (int)$aInput['content_id'] : 0 )));
+        if (count($aValues) > 0){
+            $aVars = array('bx_repeat:cats' => array());
+            foreach ($aValues as  $sValue) {
+                $aVars['bx_repeat:cats'][] = array(
+                    'url' => $oCategories->getUrl($this->_oModule->getName(), $sValue),
+                    'name' => _t($sValue),
+                    'bx_if:more' => array(
+                        'condition' => $sValue === end($aValues) ? false : true,
+                        'content' => array('1')
+                    ),
+                );
+            }
+            
+            if (!$aVars['bx_repeat:cats'])
+                return '';
+
+            return $this->_oModule->_oTemplate->parseHtmlByName('category_list_inline.html', $aVars);
+        }
+        return '';
+    }
+    
+    protected function genCustomInputMulticat(&$aInput)
+    {
+        $sJsObject = $this->_oModule->_oConfig->getJsObject('categories');
+		$aValuesForSelect = BxDolCategories::getInstance()->getData(array('type' => 'by_module_and_author', 'module' => $this->_oModule->getName(), 'author' => bx_get_logged_profile_id()));
+        
+        $aSelectedItems = array();
+        if (isset($aInput['value']) && is_array($aInput['value']))
+		    $aInput['value'] = array_filter($aInput['value']);
+        
+        if(!empty($aInput['value'])) {
+            if (!is_array($aInput['value']))
+                $aValues = BxDolCategories::getInstance()->getData(array('type' => 'by_module_and_object', 'module' => $this->_oModule->getName(), 'object_id' => (!empty($aInput['content_id']) ? (int)$aInput['content_id'] : 0 )));
+            else
+                $aValues = $aInput['value'];
+            
+            $aValues = array_filter($aValues);
+            foreach($aValues as $sValue) {
+                if (!array_key_exists($sValue, $aValuesForSelect)){
+                    $aValuesForSelect[$sValue] = array('key' => $sValue, 'value' => $sValue);
+                }
+            }
+            foreach($aValues as $sValue) {
+                $sInput = $this->genCustomInputMulticatSelect($aInput, $aValuesForSelect, $sValue);
+                $aSelectedItems[] = array('js_object' => $sJsObject, 'select_cat' => $sInput);
+            }
+        }
+        else{
+            $aSelectedItems = array(
+                array('js_object' => $sJsObject, 'select_cat' => $this->genCustomInputMulticatSelect($aInput, $aValuesForSelect))
+            );
+        }
+        return $this->_oModule->_oTemplate->parseHtmlByName('form_categories.html', array(
+            'bx_repeat:items' => $aSelectedItems,
+            'js_object' => $sJsObject, 
+            'select_cat' => $this->genCustomInputMulticatSelect($aInput, $aValuesForSelect, -1),
+            'input_cat' => $this->genCustomInputMulticatInput($aInput),
+            'js_object' => $sJsObject,
+            'btn_add' => $this->genCustomInputMulticatButton($aInput),
+            'btn_add_new' => $this->genCustomInputMulticatButtonNew($aInput)
+        ));
+    }
+    
+    protected function genCustomInputMulticatSelect($aInput, $aValues, $mixedValue = '')
+    {
+        foreach($aValues as $sKey => $aValue) {
+            $aValues[$sKey]['value'] = _t($aValue['key']);
+        }
+        $aValues = array_merge(array('1' => array('key' => false, 'value' =>  _t('_sys_please_select'))), $aValues);
+        
+        $aInput['type'] = 'select';
+        $aInput['name'] .= '[]';
+        $aInput['value'] = $mixedValue;
+        $aInput['values'] = $aValues;
+        return $this->genInput($aInput);
+    }
+    
+    protected function genCustomInputMulticatInput($aInput)
+    {
+        $aInput['type'] = 'text';
+        $aInput['name'] .= '[]';
+        $aInput['value'] = '';
+        return $this->genInput($aInput);
+    }
+    
+    protected function genCustomInputMulticatButton($aInput)
+    {
+        $sName = $aInput['name'];
+        $aInput['type'] = 'button';
+        $aInput['name'] .= '_add';
+        $aInput['value'] = _t('_sys_txt_categories_button_caption_add');
+        $aInput['attrs']['class'] = 'bx-def-margin-right bx-def-margin-sec-top';
+        $aInput['attrs']['onclick'] = $this->_oModule->_oConfig->getJsObject('categories') . ".categoryAdd(this, '" . $sName . "');";
+        return $this->genInputButton($aInput);
+    }
+    
+    protected function genCustomInputMulticatButtonNew($aInput)
+    {
+        $sName = $aInput['name'];
+        $aInput['type'] = 'button';
+        $aInput['name'] .= '_add';
+        $aInput['value'] = _t('_sys_txt_categories_button_caption_addnew');
+        $aInput['attrs']['class'] = 'bx-def-margin-sec-top';
+        $aInput['attrs']['onclick'] = $this->_oModule->_oConfig->getJsObject('categories') . ".categoryAddNew(this, '" . $sName . "');";
+        return $this->genInputButton($aInput);
+    }
+    
+    protected function _isMulticatEnabled(){
+        $CNF = $this->_oModule->_oConfig->CNF;
+        return isset($CNF['PARAM_MULTICAT_ENABLED']) && $CNF['PARAM_MULTICAT_ENABLED'] === true && isset($CNF['FIELD_MULTICAT']);
+    }
+}
+
+/** @} */

+ 646 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,646 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Entry forms helper functions
+ */
+class BxBaseModGeneralFormsEntryHelper extends BxDolProfileForms
+{
+    protected $_oModule;
+
+    /**
+     * 'Ajax Mode' determines the format of response. If it's TRUE the response 
+     * (a form or an error appeared during form creation) should be returned 
+     * as text during initial loading, while all other responses appeared after 
+     * form submit should be arrays, which are ready to path to echoJson.
+     */
+    protected $_bAjaxMode;
+
+    /**
+     * Use absolute Action URL in generated form object. 
+     * It's needed in Ajax Mode.
+     */
+    protected $_bAbsoluteActionUrl;
+
+    protected $_bDynamicMode;
+
+    public function __construct($oModule)
+    {
+        parent::__construct();
+        $this->_oModule = $oModule;
+
+        $this->_bDynamicMode = false;
+
+        $this->_bAjaxMode = false;
+        $mixedAjaxMode = bx_get('ajax_mode');
+        if($mixedAjaxMode !== false)
+            $this->setAjaxMode($mixedAjaxMode);
+
+        $this->_bAbsoluteActionUrl = false;
+        $mixedAbsoluteActionUrl = bx_get('absolute_action_url');
+        if($mixedAbsoluteActionUrl !== false)
+            $this->setAbsoluteActionUrl($mixedAbsoluteActionUrl);
+    }
+
+    public function setAjaxMode($bAjaxMode)
+    {
+        $this->_bAjaxMode = (bool)$bAjaxMode;
+        if($this->_bAjaxMode)
+            $this->setDynamicMode(true);
+    }
+
+    public function setAbsoluteActionUrl($bAbsoluteActionUrl)
+    {
+        $this->_bAbsoluteActionUrl = (bool)$bAbsoluteActionUrl;
+    }
+
+    public function setDynamicMode($bDynamicMode)
+    {
+        $this->_bDynamicMode = (bool)$bDynamicMode;
+    }
+
+    public function getObjectStorage()
+    {
+        return BxDolStorage::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_STORAGE']);
+    }
+
+    public function getObjectFormAdd ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('add', $oForm);
+
+        return $oForm; 
+    }
+
+    public function getObjectFormEdit ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('edit', $oForm);
+
+        return $oForm;
+    }
+
+    public function getObjectFormView ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormDelete ($sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('delete', $oForm);
+
+        return $oForm;
+    }
+
+    public function viewDataEntry ($iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return MsgBox($sMsg);
+
+        $oForm = $this->getObjectFormView();
+        if (!$oForm)
+            return '';
+
+        $oForm->initChecker($aContentInfo);
+
+        if(!empty($CNF['FIELD_TEXT']) &&  !$oForm->isInputVisible($CNF['FIELD_TEXT']))
+            return '';
+
+        return $this->_oModule->_oTemplate->entryText($aContentInfo);
+    }
+
+    public function addData ($iProfile, $aValues, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return array('code' => 1, 'message' => '_sys_txt_error_occured');
+
+        $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+        $oForm->aParams['csrf']['disable'] = true;
+        if(!empty($oForm->aParams['db']['submit_name'])) {            
+            $sSubmitName = false;
+            if (is_array($oForm->aParams['db']['submit_name'])) {
+                foreach ($oForm->aParams['db']['submit_name'] as $sVal) {
+                    if (isset($oForm->aInputs[$sVal])) {
+                        $sSubmitName = $sVal;
+                        break;
+                    }
+                }
+            } 
+            else {
+                $sSubmitName = $oForm->aParams['db']['submit_name'];
+            }
+            if ($sSubmitName && isset($oForm->aInputs[$sSubmitName]))
+                $aValues[$sSubmitName] = $oForm->aInputs[$sSubmitName]['value'];
+        }
+
+        $oForm->initChecker(array(), $aValues);
+        if (!$oForm->isSubmittedAndValid()) {
+            $aErrors = array();
+            array_walk($oForm->aInputs, function($aInput, $sKey) use (&$aErrors) {
+                if(!empty($aInput['error']))
+                    $aErrors[$sKey] = $aInput['error'];
+            });
+
+            return array('code' => 2, 'message' => '_sys_txt_error_occured', 'errors' => $aErrors);
+        }
+
+        // insert data into database
+        $aValsToAdd = array ();
+        if(isset($CNF['FIELD_AUTHOR']))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = $iProfile;
+
+        $iContentId = $oForm->insert($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return array('code' => 2, 'message' => '_sys_txt_error_occured');
+            else
+                return array('code' => 3, 'message' => '_sys_txt_error_entry_creation');
+        }
+
+        $sResult = $this->onDataAddAfter(BxDolProfile::getInstance($iProfile)->getAccountId(), $iContentId);
+        if($sResult)
+            return array('code' => 4, 'message' => $sResult);
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        /*
+         * Process metas.
+         * Note. It's essential to process metas a the very end, 
+         * because all data related to an entry should be already
+         * processed and are ready to be passed to alert. 
+         */
+        $this->_oModule->processMetasAdd($iContentId);
+
+        /*
+         * Create alert about the completed action.
+         */
+        $this->_oModule->alertAfterAdd($aContentInfo);
+
+        return array('code' => 0, 'message' => '', 'content' => $aContentInfo);
+    }
+
+    public function addDataForm ($sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedAdd';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $bAsJson = false;
+
+        // get form object
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return $this->prepareResponse(MsgBox(_t('_sys_txt_error_occured')), $bAsJson, 'msg');
+
+        $bAsJson = $this->_bAjaxMode && $oForm->isSubmitted();
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction())) {
+            $oProfile = BxDolProfile::getInstance();
+            if ($oProfile && ($aProfileInfo = $oProfile->getInfo()) && $aProfileInfo['type'] == 'system' && is_subclass_of($this->_oModule, 'BxBaseModProfileModule') && $this->_oModule->serviceActAsProfile()) // special check for system profile is needed, because of incorrect error message
+                return $this->prepareResponse(MsgBox(_t('_sys_txt_access_denied')), $bAsJson, 'msg');
+            else
+                return $this->prepareResponse(MsgBox($sMsg), $bAsJson, 'msg');
+        }
+
+        // check and display form
+        $oForm->initChecker();
+        if (!$oForm->isSubmittedAndValid())
+            return $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', array(
+            	'form_id' => $oForm->getId()
+            ));
+
+        // insert data into database
+        $aValsToAdd = array ();
+        $iContentId = $oForm->insert ($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', array(
+                    'form_id' => $oForm->getId()
+                ));
+            else
+                return $this->prepareResponse(MsgBox(_t('_sys_txt_error_entry_creation')), $bAsJson, 'msg');
+        }
+
+        $sResult = $this->onDataAddAfter (getLoggedId(), $iContentId);
+        if ($sResult)
+            return $this->prepareResponse($sResult, $bAsJson, 'msg');
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        /*
+         * Process metas.
+         * Note. It's essential to process metas a the very end, 
+         * because all data related to an entry should be already
+         * processed and are ready to be passed to alert. 
+         */
+        $this->_oModule->processMetasAdd($iContentId);
+
+        // Perform ACL action
+        $this->_oModule->$sCheckFunction(true);
+
+        // Create alert about the completed action.
+        $this->_oModule->alertAfterAdd($aContentInfo);
+
+        // Redirect
+        $this->redirectAfterAdd($aContentInfo);
+    }
+
+    public function redirectAfterAdd($aContentInfo)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	$sUrl = 'page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']];
+        if($this->_bAjaxMode) {
+            echoJson($this->prepareResponse($sUrl, $this->_bAjaxMode, 'redirect'));
+            exit;
+        }
+        else
+            $this->_redirectAndExit($sUrl);
+    }
+
+    public function editDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false, $bErrorMsg = true)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedEdit';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return $bErrorMsg ? MsgBox(_t('_sys_txt_error_entry_is_not_defined')) : '';
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return $bErrorMsg ? MsgBox($sMsg) : '';
+
+        // check and display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return $bErrorMsg ? MsgBox(_t('_sys_txt_error_occured')) : '';
+
+        $aSpecificValues = array();        
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->locationsIsEnabled())
+                $aSpecificValues = $oMetatags->locationGet($iContentId, empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+        $oForm->initChecker($aContentInfo, $aSpecificValues);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        // update data in the DB
+        $aTrackTextFieldsChanges = null;
+
+        $this->onDataEditBefore ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+
+        if (!$oForm->update ($aContentInfo[$CNF['FIELD_ID']], array(), $aTrackTextFieldsChanges)) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_entry_update'));
+        }
+
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $sResult = $this->onDataEditAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+        if ($sResult)
+            return $sResult;
+
+        /*
+         * Process metas.
+         * Note. It's essential to process metas a the very end, 
+         * because all data related to an entry should be already
+         * processed and are ready to be passed to alert. 
+         */
+        $this->_oModule->processMetasEdit($iContentId, $oForm);
+
+        // Perform ACL action
+        $this->_oModule->$sCheckFunction($aContentInfo, true);
+
+        // Create alert about the completed action.
+        $this->_oModule->alertAfterEdit($aContentInfo);
+        
+        // Redirect
+        $this->redirectAfterEdit($aContentInfo);
+    }
+
+    protected function redirectAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function deleteDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedDelete';
+        
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormDelete($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker($aContentInfo);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        if ($sError = $this->deleteData($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile, $oForm))
+            return MsgBox($sError);
+
+        // perform action
+        $this->_oModule->$sCheckFunction($aContentInfo, true);
+
+        // redirect
+        $this->redirectAfterDelete($aContentInfo);
+    }
+
+    protected function redirectAfterDelete($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit($CNF['URL_HOME'], true, array(
+            'account_id' => getLoggedId(),
+            'profile_id' => bx_get_logged_profile_id(),
+        ));
+    }
+
+    /**
+     * Delete data entry
+     * @param $iContentId entry id
+     * @param $oForm optional content info array
+     * @param $aContentInfo optional content info array
+     * @param $oProfile optional content author profile
+     * @return error string on error or empty string on success
+     */
+    public function deleteData ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$aContentInfo || !$oProfile)
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if (!$aContentInfo)
+            return _t('_sys_txt_error_entry_is_not_defined');
+
+        if (!$oForm)
+            $oForm = $this->getObjectFormDelete();
+
+        if (!$oForm->delete ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo))
+            return _t('_sys_txt_error_entry_delete');
+
+        // remove data from nested forms 
+        $aNestedForms = BxDolFormQuery::getNestedFormObjects($oForm->aParams['object']);
+        foreach ($aNestedForms as $aNestedForm) {
+            BxDolFormQuery::deleteDataFromNestedForm($aNestedForm['table'], $iContentId);
+        }
+        
+        if ($sResult = $this->onDataDeleteAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile))
+            return $sResult;
+
+        // create an alert
+        bx_alert($this->_oModule->getName(), 'deleted', $aContentInfo[$CNF['FIELD_ID']], false, array(
+            'content' => &$aContentInfo
+        ));
+
+        return '';
+    }
+
+    public function viewDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if ($sMsg = $this->_processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile))
+            return MsgBox($sMsg);
+
+        // get form
+        $oForm = $this->getObjectFormView($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        // process metatags
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = $oMetatags->metaFields($aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']);
+                $oForm->setMetatagsKeywordsData($iContentId, $aFields, $oMetatags);
+            }
+        }        
+
+        // display profile
+        $oForm->initChecker($aContentInfo);
+        return $oForm->getCode();
+    }
+
+    protected function _processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return $sMsg;
+
+        return '';
+    }
+
+    public function onDataDeleteAfter ($iContentId, $aContentInfo, $oProfile)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->deleteGroupCustomByContentId($iContentId);
+
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_detete_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    public function onDataEditBefore ($iContentId, $aContentInfo, &$aTrackTextFieldsChanges, &$oProfile, &$oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]) && $aContentInfo[$CNF['FIELD_STATUS']] == 'failed')
+            $oForm->addTrackFields($CNF['FIELD_STATUS'], $aContentInfo);
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        /*
+         * Load update data.
+         */
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if(isset($CNF['FIELD_PHOTO']))
+            $oForm->processFiles ($CNF['FIELD_PHOTO'], $iContentId, false);
+
+        if(isset($CNF['FIELD_STATUS']) && ($aTrackResult = $oForm->isTrackFieldChanged($CNF['FIELD_STATUS'], true)) !== false)
+            if($aTrackResult['old'] == 'failed' && $aTrackResult['new'] == 'active')
+                $this->_oModule->alertAfterAdd($aContentInfo);
+
+        if(isset($CNF['FIELD_ALLOW_VIEW_TO']) && !empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->reassociateGroupCustomWithContent($oProfile->id(), $iContentId, (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+        
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_edit_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if(($oForm = $this->getObjectFormAdd()) !== false) {
+            if(isset($CNF['FIELD_PHOTO']))
+                $oForm->processFiles($CNF['FIELD_PHOTO'], $iContentId, true);
+        }
+
+        if(isset($CNF['FIELD_ALLOW_VIEW_TO']) && !empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->associateGroupCustomWithContent($oProfile->id(), $iContentId, (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_add_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    protected function prepareCustomRedirectUrl($s, $aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aContentInfo[$CNF['FIELD_ID']], $this->_oModule->getName());
+
+        $aMarkers = array(
+            '{profile_id}',
+            '{content_id}',
+            '{module}',
+        );
+        $aReplacements = array(
+            $oProfile ? $oProfile->id() : 0,
+            $aContentInfo[$CNF['FIELD_ID']],
+            $this->_oModule->getName(),
+        );
+        $s = str_replace($aMarkers, $aReplacements, $s);
+
+        $s = BxDolPermalinks::getInstance()->permalink($s);
+
+        if (false === strpos($s, 'http://') && false === strpos($s, 'https://'))
+            $s = BX_DOL_URL_ROOT . $s;
+        
+        return $s;
+    }
+    
+    protected function prepareResponse($mixedResponse, $bAsJson = false, $sKey = 'msg', $aAdditional = array())
+    {
+    	if(!$bAsJson)
+            return $mixedResponse;
+
+        $aResponse = array(
+            $sKey => $mixedResponse,
+            '_dt' => 'json'
+        );
+
+        if(!empty($aAdditional) && is_array($aAdditional))
+            $aResponse = array_merge($aResponse, $aAdditional);
+
+        return $aResponse;
+    }
+    
+    protected function _setAbsoluteActionUrl($sType, &$oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sKeyUri = 'URI_' . strtoupper($sType) . '_ENTRY';
+        if(empty($CNF[$sKeyUri]))
+            return;
+
+        $oForm->setAbsoluteActionUrl(BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF[$sKeyUri]));
+    }
+}
+
+/** @} */

+ 295 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralGridAdministration.php

@@ -0,0 +1,295 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ * @{
+ */
+
+define('BX_DOL_MANAGE_TOOLS_ADMINISTRATION', 'administration');
+define('BX_DOL_MANAGE_TOOLS_COMMON', 'common');
+
+class BxBaseModGeneralGridAdministration extends BxTemplGrid
+{
+	protected $MODULE;
+	protected $_oModule;
+
+	protected $_sManageType;
+	protected $_sParamsDivider;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_sManageType = BX_DOL_MANAGE_TOOLS_ADMINISTRATION;
+        $this->_sParamsDivider = '#-#';
+
+        $this->_sDefaultSortingOrder = 'DESC';
+    }
+
+	public function performActionDelete($aParams = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $aIdsAffected = array ();
+        foreach($aIds as $iId) {
+			$aContentInfo = $this->_getContentInfo($iId);
+	    	if($this->_oModule->checkAllowedDelete($aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+	    		continue;
+
+        	if(!$this->_doDelete($iId, $aParams))
+                continue;
+
+			if(!$this->_onDelete($iId, $aParams))
+				continue;
+
+			$this->_oModule->checkAllowedDelete($aContentInfo, true);
+
+            $aIdsAffected[] = $iId;
+            $iAffected++;
+        }
+
+        echoJson($iAffected ? array('grid' => $this->getCode(false), 'blink' => $aIdsAffected) : array('msg' => _t($CNF['T']['grid_action_err_delete'])));
+    }
+    
+    public function performActionClearReports($aParams = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $aIdsAffected = array ();
+        foreach($aIds as $iId) {
+            if (isset($CNF['OBJECT_REPORTS'])){
+                $oReport = BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $iId);
+                $oReport->actionClearReport();
+                $aIdsAffected[] = $iId;
+                $iAffected++;
+            }
+        }
+
+        echoJson($iAffected ? array('grid' => $this->getCode(false), 'blink' => $aIdsAffected) : array('msg' => _t($CNF['T']['grid_action_err_clear_reports'])));
+    }
+
+    protected function _getActionAuditContent($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!getParam('sys_audit_enable') || getParam('sys_audit_acl_levels') == '')
+            return;
+        
+        $iProfileId = bx_get_logged_profile_id();
+        if (!BxDolAcl::getInstance()->isMemberLevelInSet(explode(',', getParam('sys_audit_acl_levels')), $iProfileId))
+            return;
+    	
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+        $sUrl = BX_DOL_URL_ROOT . 'page/audit-administration?module=' . $this->_oModule->getName() . '&content_id=' . $aRow[$CNF['FIELD_ID']];
+
+    	$a['attr'] = array_merge($a['attr'], array(
+    		"onclick" => "window.open('" . $sUrl . "','_audit');"
+    	));
+
+    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionDelete($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+		if($this->_sManageType == BX_DOL_MANAGE_TOOLS_ADMINISTRATION && $this->_oModule->checkAllowedEditAnyEntry() !== CHECK_ACTION_RESULT_ALLOWED)
+			return '';
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionSettings($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+    	$sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+    	$sMenuName = $this->_oModule->_oConfig->CNF['OBJECT_MENU_MANAGE_TOOLS'];
+
+    	$oMenu = BxDolMenu::getObjectInstance($sMenuName);
+    	$oMenu->setContentId($aRow['id']);
+
+    	$sMenu = $oMenu->getCode();
+    	if(empty($sMenu))
+    		return '';
+
+    	$a['attr'] = array_merge($a['attr'], array(
+    		"bx-popup-id" => $sMenuName . "-" . $aRow['id'],
+    		"onclick" => "$(this).off('click'); " . $sJsObject . ".onClickSettings('" . $sMenuName . "', this);"
+    	));
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getFilterSelectOne($sFilterName, $sFilterValue, $aFilterValues)
+    {
+        if(empty($sFilterName) || empty($aFilterValues))
+            return '';
+
+		$CNF = &$this->_oModule->_oConfig->CNF;
+		$sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+
+		foreach($aFilterValues as $sKey => $sValue)
+			$aFilterValues[$sKey] = _t($sValue);
+
+        $aInputModules = array(
+            'type' => 'select',
+            'name' => $sFilterName,
+            'attrs' => array(
+                'id' => 'bx-grid-' . $sFilterName . '-' . $this->_sObject,
+                'onChange' => 'javascript:' . $sJsObject . '.onChangeFilter(this)'
+            ),
+            'value' => $sFilterValue,
+            'values' => array_merge(array('' => _t($CNF['T']['filter_item_select_one_' . $sFilterName])), $aFilterValues)
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInputModules);
+    }
+
+	protected function _getSearchInput()
+    {
+        $sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+
+        $aInputSearch = array(
+            'type' => 'text',
+            'name' => 'search',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject,
+                'onKeyup' => 'javascript:$(this).off(\'keyup focusout\'); ' . $sJsObject . '.onChangeFilter(this)',
+                'onBlur' => 'javascript:' . $sJsObject . '.onChangeFilter(this)',
+            )
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInputSearch);
+    }
+
+	protected function _getContentInfo($iId)
+    {
+    	return $this->_oModule->_oDb->getContentInfoById($iId);
+    }
+    
+    protected function _getProfileObject($iId)
+    {
+        return BxDolProfile::getInstanceMagic($iId);
+    }
+
+	protected function _getManageAccountUrl($sFilter = '')
+    {
+    	$sModuleAccounts = 'bx_accounts';
+    	if(!BxDolModuleQuery::getInstance()->isEnabledByName($sModuleAccounts))
+    		return '';
+
+		$sTypeUpc = strtoupper($this->_sManageType);
+		$oModuleAccounts = BxDolModule::getInstance($sModuleAccounts);
+		if(!$oModuleAccounts || empty($oModuleAccounts->_oConfig->CNF['URL_MANAGE_' . $sTypeUpc]))
+			return '';
+
+		$sLink = $oModuleAccounts->_oConfig->CNF['URL_MANAGE_' . $sTypeUpc];
+
+		$sLink = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($sLink);
+		
+		if(!empty($sFilter))
+			$sLink = bx_append_url_params($sLink, array('filter' => $sFilter));
+
+		return $sLink;
+    }
+
+    protected function _enable ($mixedId, $isChecked)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $mixedResult = parent::_enable($mixedId, $isChecked);
+        if((int)$mixedResult > 0) {
+            if(!empty($CNF['FIELD_CHANGED']))
+                $this->_oModule->_oDb->updateEntriesBy(array($CNF['FIELD_CHANGED'] => time()), array($this->_aOptions['field_id'] => $mixedId));
+
+            $aContentInfo = $this->_oModule->_oDb->getContentInfoById($mixedId);
+
+            $this->_oModule->alertAfterEdit($aContentInfo);
+            
+            $iContextId = isset($CNF['FIELD_ALLOW_VIEW_TO']) && (!empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) ? - $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] : 0;
+            $AuditParams = array(
+                'content_title' => (isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']])) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+                'context_profile_id' => $iContextId,
+                'content_info_object' =>  isset($CNF['OBJECT_CONTENT_INFO']) ? $CNF['OBJECT_CONTENT_INFO'] : '',
+                'data' => $aContentInfo
+            );
+            if ($iContextId > 0)
+                $AuditParams['context_profile_title'] = BxDolProfile::getInstance($iContextId)->getDisplayName();
+            
+            bx_audit(
+                $mixedId, 
+                $this->_oModule->getName(), 
+                ($isChecked ? '_sys_audit_action_content_enabled': '_sys_audit_action_content_disabled'), 
+                $AuditParams
+            );
+        }
+
+        return $mixedResult;
+    }
+
+    protected function _doDelete($iId, $aParams = array())
+    {
+    	return $this->_oModule->serviceDeleteEntity($iId) == '';
+    }
+
+    protected function _onDelete($iId, $aParams = array())
+    {
+    	return true;
+    }
+
+    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($CNF['FIELD_STATUS']) && isset($aRow[$CNF['FIELD_STATUS']]) && !in_array($aRow[$CNF['FIELD_STATUS']], array('active', 'hidden'))) {
+            $sStatusKey = '_sys_status_' . $aRow[$CNF['FIELD_STATUS']];
+            if(!empty($CNF['T']['txt_status_' . $aRow[$CNF['FIELD_STATUS']]]))
+                $sStatusKey = $CNF['T']['txt_status_' . $aRow[$CNF['FIELD_STATUS']]];
+
+            return parent::_getCellDefault(_t($sStatusKey), $sKey, $aField, $aRow);
+        }
+
+        return parent::_getCellSwitcher ($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellHeaderReports ($sKey, $aField)
+    {
+        $s = parent::_getCellHeaderDefault($sKey, $aField);
+        return preg_replace ('/<a(.*?)>(.*?)<\/a>/', '<a$1 title="' . bx_html_attribute(_t('_sys_txt_reports_title')) . '"><i class="sys-icon exclamation-triangle"></i></a>', $s);
+    }
+    
+    protected function _getCellReports($mixedValue, $sKey, $aField, $aRow)
+    {
+        if ($mixedValue == 0){
+            $mixedValue = '';
+        }
+        else{
+            $CNF = &$this->_oModule->_oConfig->CNF;
+            $oReports = isset($CNF['OBJECT_REPORTS']) ? BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $aRow[$CNF['FIELD_ID']]) : null;
+            if ($oReports){
+                $mixedValue = $oReports->getCounter().$oReports->getJsScript();
+            }
+        }
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+}
+
+/** @} */

+ 28 - 0
upgrade/files/11.0.4-12.0.0.B1/files/modules/base/general/classes/BxBaseModGeneralMenuSubmenuMoreAuto.php

@@ -0,0 +1,28 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * View module submenu
+ */
+class BxBaseModGeneralMenuSubmenuMoreAuto extends BxTemplMenuMoreAuto
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+    }
+}
+
+/** @} */

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