فهرست منبع

Upgrade script from 9.0.0.B3 to 9.0.0.B4

Alex Trofimov 9 سال پیش
والد
کامیت
1cbe6ed5b4
100فایلهای تغییر یافته به همراه28535 افزوده شده و 0 حذف شده
  1. 9 0
      upgrade/files/9.0.0.B3-9.0.0.B4/check.php
  2. 30 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/feature.php
  3. 428 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolAccount.php
  4. 711 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolAcl.php
  5. 86 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolCronAccount.php
  6. 118 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolCronPruning.php
  7. 1115 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolDb.php
  8. 240 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFavorite.php
  9. 196 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFeature.php
  10. 61 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFeatureQuery.php
  11. 124 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFileHandler.php
  12. 37 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFileHandlerQuery.php
  13. 172 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolGridConnections.php
  14. 299 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolMenu.php
  15. 598 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolMetatags.php
  16. 62 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolMinify.php
  17. 293 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolObject.php
  18. 302 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPayments.php
  19. 271 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPermalinks.php
  20. 348 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPrivacy.php
  21. 196 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolSession.php
  22. 1411 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorage.php
  23. 380 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorageQuery.php
  24. 223 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorageS3.php
  25. 2425 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolTemplate.php
  26. 443 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolUploader.php
  27. 187 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolView.php
  28. 81 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolViewQuery.php
  29. 317 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolVote.php
  30. 116 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolFeature.js
  31. 52 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolView.js
  32. 176 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolVote.js
  33. 733 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/functions.js
  34. 23 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/jquery.dolEmbedly.js
  35. 582 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/jquery.dolPopup.js
  36. 284 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/profiles.inc.php
  37. 1522 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/utils.inc.php
  38. 12 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/version.inc.php
  39. 290 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/connect/classes/BxBaseModConnectModule.php
  40. 210 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php
  41. 343 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  42. 936 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralModule.php
  43. 37 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/template/css/main.css
  44. 1 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/template/entry-share.html
  45. 35 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsFormEntry.php
  46. 87 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsFormsEntryHelper.php
  47. 25 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsGridAdministration.php
  48. 29 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsGridCommon.php
  49. 503 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  50. 128 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsModule.php
  51. 49 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsResponse.php
  52. 100 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsTemplate.php
  53. 162 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/payment/classes/BxBaseModPaymentConfig.php
  54. 168 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/profile/classes/BxBaseModProfileAlertsResponse.php
  55. 661 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/profile/classes/BxBaseModProfileModule.php
  56. 147 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextFormEntry.php
  57. 149 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextGridAdministration.php
  58. 35 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextMenu.php
  59. 25 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextMenuView.php
  60. 205 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextModule.php
  61. 67 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextPageEntry.php
  62. 303 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextTemplate.php
  63. 28 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/attachments.html
  64. 1 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/author_link.html
  65. 225 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/css/main.css
  66. 8 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/unit_full_private.html
  67. 10 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/unit_gallery_private.html
  68. 393 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/AuthorizeController.php
  69. 111 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/ResourceController.php
  70. 295 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/TokenController.php
  71. 100 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/GrantType/AuthorizationCode.php
  72. 24 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/OpenID/ResponseType/CodeIdToken.php
  73. 124 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/OpenID/ResponseType/IdToken.php
  74. 369 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Response.php
  75. 194 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/ResponseType/AccessToken.php
  76. 100 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/ResponseType/AuthorizationCode.php
  77. 879 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Server.php
  78. 64 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/AccessTokenInterface.php
  79. 86 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/AuthorizationCodeInterface.php
  80. 480 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Cassandra.php
  81. 540 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/DynamoDB.php
  82. 381 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Memory.php
  83. 392 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Mongo.php
  84. 380 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/MongoDB.php
  85. 553 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Pdo.php
  86. 321 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Redis.php
  87. 82 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/RefreshTokenInterface.php
  88. 2390 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/amazon-s3/S3.php
  89. 113 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee.php
  90. 173 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Model.php
  91. 51 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Addon.php
  92. 26 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Address.php
  93. 41 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Card.php
  94. 46 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Coupon.php
  95. 36 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CouponCode.php
  96. 41 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNote.php
  97. 16 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteEstimate.php
  98. 9 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteEstimateLineItem.php
  99. 9 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteLineItem.php
  100. 86 0
      upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Customer.php

+ 9 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/check.php

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

+ 30 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/feature.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']) : '';
+
+$oFeature = BxDolFeature::getObjectInstance($sSys, $iObjectId, true);
+
+if ($oFeature && $sSys && $iObjectId && $sAction) {
+    header('Content-Type: text/html; charset=utf-8');
+    $sMethod = 'action' . ucfirst($sAction);
+    if(method_exists($oFeature, $sMethod))
+        echo $oFeature->$sMethod();
+}
+
+/** @} */

+ 428 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolAccount.php

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

+ 711 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolAcl.php

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

+ 86 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolCronAccount.php

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

+ 118 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolCronPruning.php

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

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

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

+ 240 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFavorite.php

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

+ 196 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFeature.php

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

+ 61 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFeatureQuery.php

@@ -0,0 +1,61 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolFeature
+ */
+class BxDolFeatureQuery extends BxDolObjectQuery
+{
+    protected $_sTriggerFieldFlag;
+
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_sTriggerFieldFlag = isset($aSystem['trigger_field_flag']) ? $aSystem['trigger_field_flag'] : '';
+
+        $this->_sMethodGetEntry = 'getFeature';
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        return (int)$this->getObjectFlag($iObjectId) > 0;
+    }
+
+    public function getPerformedBy($iObjectId)
+    {
+        return array();
+    }
+
+    public function getObjectFlag($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldFlag}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return (int)$this->getOne($sQuery);
+    }
+
+    public function updateTriggerTableValue($iObjectId, $iValue)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldFlag}` = ? WHERE `{$this->_sTriggerFieldId}` = ?", (int)$iValue, $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    protected function _updateTriggerTable($iObjectId, $aEntry)
+    {
+    	$sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldFlag}` = ? WHERE `{$this->_sTriggerFieldId}` = ?", $aEntry['count'], $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+	public function getFeature($iObjectId)
+    {
+        return array('count' => $this->getObjectFlag($iObjectId));
+    }
+}
+
+/** @} */

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

+ 37 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolFileHandlerQuery.php

@@ -0,0 +1,37 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for file handlers objects.
+ * @see BxDolFileHandler
+ */
+class BxDolFileHandlerQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_file_handlers` ORDER BY `order` ASC");
+        $aObjects = $oDb->getAllWithKey($sQuery, 'object');
+        if (!$aObjects || !is_array($aObjects))
+            return false;
+
+        return $aObjects;
+    }
+
+}
+
+/** @} */

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

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

+ 299 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolMenu.php

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

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

@@ -0,0 +1,598 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_METATAGS_KEYWORDS_MAX', 9);
+
+/**
+ * @page objects
+ * @section metatags Meta-tags
+ * @ref BxDolMetatags
+ */
+
+// TODO: client side controls: some js to help to enter \@mentions and \#keywords(optional)
+// TODO: integrate to comments, try to integrate to one metatags object - maybe specify metatags object in comments object and make appropriate changes in comments, 
+//       comments content should be treated as main conetent and main content should be shown in search results
+// TODO: integration with notifications, when smbd is \@mentioned
+
+/**
+ * Meta-tags for different content. It can handle \#keywords, \@mentions, locations and meta image for the content.
+ *
+ * Keywords are parsed after content add and edit and added to the database, 
+ * later when content is displayed \#keywords are highlighted as links to the search page with all content with this keyword, 
+ * also keywords are displayed as meta info in page header.
+ * 
+ * Location info upon content adding/editing is displayed as custom field, it detects current user location and attach location as hidden form fields.
+ * When content is displayed location can be shown as clickable links to search page where results are other items from same location,
+ * also location is displayed as meta info in page header as latutude and longitude.
+ * 
+ * Content image can be added as meta info using this class as well.
+ *
+ *
+ *
+ * @section metatags_create Add new meta-tags object.
+ *
+ * To add new meta-tags object insert new record into `sys_objects_metatags` table:
+ * - object: name of the meta-tags object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing
+ * - table_keywords: table name to store keywords, leave empty to disable keywords
+ * - table_locations: table name to store locations, leave empty to disable locations
+ * - table_mentions: table name to store mentions, leave empty to disable mentions
+ * - override_class_name: class name to override  
+ * - override_class_file: class file path to override
+ *
+ * To simplify operations with metatags in modules which are derived from some 'base' modules, specify it in module config class in 'CNF' array: 
+ * - FIELDS_WITH_KEYWORDS: array or comma separated list of field names with keywords support, or 'auto' to automatically use all 'textarea' fields
+ * - OBJECT_METATAGS: metatags object name
+ *
+ *
+ *
+ * @section metatags_locations Location
+ *
+ * Upon content 'add' and 'edit' form submit, call BxDolMetatags::locationsAddFromForm
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->locationsAddFromForm($iContentId);
+ * @endcode
+ *
+ * Upon 'edit' form display call BxDolMetatags::locationGet to fill-in location form input, for example:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $aSpecificValues = $oMetatags->locationGet($iContentId);
+ *   $oForm->initChecker($aContentInfo, $aSpecificValues);
+ * @endcode
+ *
+ * To display location call BxDolMetatags::locationsString:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   echo $oMetatags->locationsString($iContentId)
+ * @endcode
+ *
+ * Upon page display call BxDolMetatags::metaAdd to add location (and all other, including content image) meta information to the page header:
+ * @code
+ *   $o = BxDolMetatags::getObjectInstance('object_name');
+ *   $o->metaAdd($iContentId, array('id' => $iFileId, 'object' => 'storage_object_name'));
+ * @endcode
+ * 
+ * Form object must have location field, usual name for this field is 'location' (this name is used as $sPrefix in some functions), type is 'custom', 
+ * also form object must be derived from BxBaseModGeneralFormEntry class, which has custom field declaration.
+ *
+ *
+ *
+ * @section metatags_keywords \#keyword
+ *
+ * Upon content 'add' and 'edit' form submit, call BxDolMetatags::locationsAddFromForm
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->keywordsAdd($iContentId, $aContentInfo['text_field_with_hash_tags']);
+ * @endcode
+ *
+ * Before displaying the text with hashtags call BxDolMetatags::keywordsParse to highlight keywords:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $sText = $oMetatags->keywordsParse($iContentId, $sText);
+ * @endcode
+ *
+ * Upon page display call BxDolMetatags::metaAdd to add keywords (and all other, including content image) meta information to the page header.
+ *
+ * To be able to search by tags metatgs object must be specified in *SearchResult class as 'object_metatags' in 'aCurrent' array.
+ *
+ *
+ *
+ * @section metatags_mention \@mention
+ * TODO:
+ *
+ *
+ *
+ * @section metatags_delete_content Content deletion
+ * 
+ * When content is deleted associated meta data can be deleted by calling BxDolMetatags::onDeleteContent:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->onDeleteContent($iContentId);
+ * @endcode
+ *
+ */
+class BxDolMetatags extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMetas = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of metags object options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        
+        $a = array ('keywords', 'locations', 'mentions', 'pictures');
+        foreach ($a as $sMeta) {
+            if (empty($this->_aObject['table_' . $sMeta]))
+                continue;
+            $this->_aMetas[] = $sMeta;
+        }
+
+        $this->_oQuery = new BxDolMetatagsQuery($this->_aObject);
+    }
+
+    /**
+     * Get metatags object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject])) 
+            return $GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject];
+
+        $aObject = BxDolMetatagsQuery::getMetatagsObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplMetatags';
+        if (!empty($aObject['override_class_name']))
+            $sClass = $aObject['override_class_name'];
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass($aObject);
+
+        return ($GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject] = $o);
+    }
+
+    /**
+     * Add all available meta tags to the head section 
+     * @return number of successfully added metas
+     */
+    public function metaAdd($iId, $mixedImage = false)
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'AddMeta';
+            $i += $this->$sFunc($iId);
+        }
+
+        if ($mixedImage && is_array($mixedImage)) {
+            
+            if (!empty($mixedImage['object']))
+                $o = BxDolStorage::getObjectInstance($mixedImage['object']);
+            elseif (!empty($mixedImage['transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($mixedImage['transcoder']);
+
+            $mixedImage = $o ? $o->getFileUrlById($mixedImage['id']) : false;
+        }
+
+        if ($mixedImage) 
+            BxDolTemplate::getInstance()->addPageMetaImage($mixedImage);
+
+        return $i;
+    }
+
+
+    /**
+     * Checks if keywords enabled for current metatags object
+     */
+    public function keywordsIsEnabled() 
+    {
+        return empty($this->_aObject['table_keywords']) ? false : true;
+    }
+
+    /**
+     * This function is specifically for formatting "Photo Camera" string, when "Photo Camera" is used as image hashtag
+     */
+    static public function keywordsCameraModel($aExif) 
+    {
+        if (!isset($aExif['Make']))
+            return '';
+
+        $sMake = trim($aExif['Make']);
+        if ($sMake && isset($aExif['Model'])) {
+            $sModel = trim($aExif['Model']);
+            if (0 === mb_strpos($sModel, $sMake))
+                $sModel = mb_substr($sModel, mb_strlen($sMake));
+        }
+
+        return $sMake . (empty($sModel) ? '' : ' ' . trim($sModel));
+    }
+
+    /**
+     * Add \#keywords from the string
+     * @param $iId content id
+     * @param $s string with \#keywords
+     * @return number of found keywords
+     */
+    public function keywordsAdd($iId, $s) 
+    {
+        $a = array();
+        if (!preg_match_all('/[^&](\#[\pL\pN_]+)/u', ' ' . strip_tags($s), $a)) {
+            $this->_oQuery->keywordsDelete($iId);
+            return 0;
+        }
+
+        $aTags = array_unique($a[1]);
+        $aTags = array_slice($aTags, 0, BX_METATAGS_KEYWORDS_MAX);
+
+        return $this->_oQuery->keywordsAdd($iId, $aTags);
+    }
+
+    /**
+     * Add keyword from the whole string
+     * @param $iId content id
+     * @param $s keyword 
+     * @return number of added keywords, should be 1
+     */
+    public function keywordsAddOne($iId, $s, $bDeletePreviousKeywords = true)
+    {
+        if ($bDeletePreviousKeywords)
+            $this->_oQuery->keywordsDelete($iId);
+
+        return $this->_oQuery->keywordsAdd($iId, array($s));
+    }
+
+    /**
+     * Add \#keywords from the content fields
+     * @param $iId content id
+     * @param $s string with \#keywords
+     * @return number of found keywords
+     */
+    public function keywordsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) 
+    {
+        $aFields = $this->keywordsFields($aContentInfo, $CNF, $sFormDisplay); 
+        $sTextWithKeywords = '';
+        foreach ($aFields as $sField)
+            $sTextWithKeywords .= $aContentInfo[$sField];
+
+        return $this->keywordsAdd($iId, $sTextWithKeywords);
+    }
+
+    /**
+     * Get field names which are subject to parse keywords
+     */
+    public function keywordsFields($aContentInfo, $CNF, $sFormDisplay)
+    {
+        $aFields = array();
+        if (empty($CNF['FIELDS_WITH_KEYWORDS'])) {
+            return array();
+        }
+        elseif (is_string($CNF['FIELDS_WITH_KEYWORDS']) && 'auto' == $CNF['FIELDS_WITH_KEYWORDS']) {
+            if (!($oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sFormDisplay)))
+                return array();
+
+            foreach ($oForm->aInputs as $k => $a) {
+                if ('textarea' == $a['type'])
+                    $aFields[] = $a['name'];
+            }
+        } 
+        elseif (is_array($CNF['FIELDS_WITH_KEYWORDS'])) {
+            $aFields = $CNF['FIELDS_WITH_KEYWORDS'];
+        } 
+        elseif (is_string($CNF['FIELDS_WITH_KEYWORDS'])) {
+            $aFields = explode(',', $CNF['FIELDS_WITH_KEYWORDS']);
+        }
+
+        return $aFields;
+    }
+
+    /**
+     * Add links to the \#keywords in the string
+     * @param $iId content id
+     * @param $s string with \#keywords
+     * @return modified string where all \#keywords are transformed to links with rel="tag" attribute
+     */
+    public function keywordsParse($iId, $s) 
+    {   
+        $a = $this->keywordsGet($iId);
+        if (empty($a))
+            return $s;
+
+        foreach ($a as $sKeyword)
+            $s = str_ireplace('#' . $sKeyword, '<a rel="tag" href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . '"><s>#</s><b>' . $sKeyword . '</b></a>', $s);
+
+        return $s;
+    }
+
+    /**
+     * Add link to the provided keyword
+     * @param $iId content id
+     * @param $s keyword 
+     * @return modified string where provided keyword is transformed to link with rel="tag" attribute
+     */
+    public function keywordsParseOne($iId, $s) 
+    {   
+        $a = $this->keywordsGet($iId);
+        if (empty($a))
+            return $s;
+    
+        foreach ($a as $sKeyword)
+            if (0 === strcasecmp(mb_strtolower($s), mb_strtolower($sKeyword)))
+                $s = '<a rel="tag" href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . '">' . $sKeyword . '</a>';
+
+        return $s;
+    }
+
+    /**
+     * Add keywords meta info to the head section
+     * @param $iId content id
+     */
+    protected function keywordsAddMeta($iId)
+    {
+        BxDolTemplate::getInstance()->addPageKeywords($this->keywordsGet($iId));
+    }
+
+    /**
+     * Get list of keywords associated with the content
+     * @return array of keywords
+     */
+    public function keywordsGet($iId)
+    {
+        return $this->_oQuery->keywordsGet($iId);
+    }
+
+    /**
+     * Set condition for search results object for meta keyword
+     * @param $oSearchResult search results object
+     * @param $sKeyword keyword
+     */
+    public function keywordsSetSearchCondition($oSearchResult, $sKeyword)
+    {
+        $oSearchResult->aCurrent['restriction']['meta_keyword'] = array(
+            'value' => $sKeyword,
+            'field' => 'keyword',
+            'operator' => '=',
+            'table' => $this->_aObject['table_keywords'],
+        );
+
+        $oSearchResult->aCurrent['join']['meta_keyword'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_keywords'],
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+
+    public function keywordsPopularList($iLimit)
+    {
+        return $this->_oQuery->keywordsPopularList($iLimit);
+    }
+
+
+    /**
+     * Checks if locations enabled for current metatags object
+     */
+    public function locationsIsEnabled() 
+    {
+        return empty($this->_aObject['table_locations']) ? false : true;
+    }
+
+    /**
+     * Add location for the content
+     * @param $iId content id
+     * @param $sLatitude latitude
+     * @param $sLongitude longitude
+     * @param $sCountryCode optional 2 letters country code (ISO 3166-1)
+     * @param $sState optional state/province/territory name
+     * @param $sCity optional city name
+     * @param $sZip optional ZIP/postcode
+     * @param $sStreet optional street name
+     * @param $sStreetNumber optional street number
+     * @return true if location was added, or false otherwise
+     */
+    public function locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip = '', $sStreet = '', $sStreetNumber = '') 
+    {
+        return $this->_oQuery->locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber);
+    }
+
+    /**
+     * Add location for the content from POST data
+     * @param $iId content id
+     * @param $sPrefix field prefix for POST data, or empty -  if no prefix
+     * @param $oForm form to use to get POST data, or null - then new form instance will be created
+     */
+    public function locationsAddFromForm($iId, $sPrefix = '', $oForm = null)
+    {
+        if (!$this->locationsIsEnabled())
+            return;
+
+        if ($sPrefix)
+            $sPrefix .= '_';
+        if (!$oForm)
+            $oForm = new BxDolForm(array(), false);
+        $this->locationsAdd($iId, $oForm->getCleanValue($sPrefix.'lat'), $oForm->getCleanValue($sPrefix.'lng'), $oForm->getCleanValue($sPrefix.'country'), $oForm->getCleanValue($sPrefix.'state'), $oForm->getCleanValue($sPrefix.'city'), $oForm->getCleanValue($sPrefix.'zip'), $oForm->getCleanValue($sPrefix.'street'), $oForm->getCleanValue($sPrefix.'street_number'));
+    }
+
+    /**
+     * Get locations string with links
+     * @param $iId content id
+     * @return string with links to country and city
+     */
+    public function locationsString($iId, $bHTML = true)
+    {
+        bx_import('BxDolForm');
+        $aCountries = BxDolFormQuery::getDataItems('Country');
+        $aLocation = $this->locationGet($iId);
+        if(!$aLocation || !$aLocation['country'] || !isset($aCountries[$aLocation['country']]))
+            return '';
+
+        $sCountryUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country&keyword=' . $aLocation['country'] . '">' . $aCountries[$aLocation['country']] . '</a>';
+        if(empty($aLocation['state']) || empty($aLocation['city']))
+            return _t('_sys_location_country', $sCountryUrl);
+
+        $sCityUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country_city&keyword=' . $aLocation['country'] . '&state=' . rawurlencode($aLocation['state']) . '&city=' . rawurlencode($aLocation['city']) . '">' . $aLocation['city'] . '</a>';
+        $sStateUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country_state&keyword=' . $aLocation['country'] . '&state=' . rawurlencode($aLocation['state']) . '">' . $aLocation['state'] . '</a>';
+
+        if(empty($aLocation['street']) || empty($aLocation['street_number']))
+        	$s = _t('_sys_location_country_city', $sCountryUrl, $sStateUrl, $sCityUrl);
+        else
+            $s = _t('_sys_location_country_city_street', $sCountryUrl, $sStateUrl, $sCityUrl, $aLocation['street'], $aLocation['street_number']);
+
+        return $bHTML ? $s : trim(strip_tags($s));
+    }
+
+    /**
+     * Add keywords meta info to the head section
+     * @param $iId content id
+     */
+    protected function locationsAddMeta($iId) 
+    {
+        $aLocation = $this->locationGet($iId);
+        if (!empty($aLocation['lat']) && !empty($aLocation['lng']) && !empty($aLocation['country']))
+            BxDolTemplate::getInstance()->addPageMetaLocation($aLocation['lat'], $aLocation['lng'], $aLocation['country']);
+    }
+
+    /**
+     * Set condition for search results object for meta locations
+     * @param $oSearchResult search results object
+     * @param $sCountry country and other location info
+     */
+    public function locationsSetSearchCondition($oSearchResult, $sCountry, $sState = false, $sCity = false, $sZip = false)
+    {
+        if (empty($this->_aObject['table_locations'])) {
+            $oSearchResult->aCurrent['restriction']['meta_location'] = array(
+                'operator' => 'nothing',
+                'field' => 'nofield',
+                'value' => 'novalue',
+            );
+            return;
+        }
+
+        $a = array('country' => 'sCountry', 'state' => 'sState', 'city' => 'sCity', 'zip' => 'sZip');
+        foreach ($a as $sIndex => $sVar) {
+            if (!$$sVar)
+                continue;
+
+            $oSearchResult->aCurrent['restriction']['meta_location_' . $sIndex] = array(
+                'value' => $$sVar,
+                'field' => $sIndex,
+                'operator' => '=',
+                'table' => $this->_aObject['table_locations'],
+            );
+        }
+
+        $oSearchResult->aCurrent['join']['meta_location'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_locations'],
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+
+    /**
+     * Get location
+     * @param $iId content id
+     * @param $sPrefix field prefix for returning data array
+     * @return location array with the following keys (when no prefix specified): object_id, lat, lng, country, state, city, zip, street, street_number
+     */
+    public function locationGet($iId, $sPrefix = '')
+    {
+        if (!$this->locationsIsEnabled())
+            return;
+
+        $a = $this->_oQuery->locationGet($iId);
+        if (!$sPrefix)
+            return $a;
+
+        $aRet = array();
+        foreach ($a as $sKey => $sVal)
+            $aRet[$sPrefix . '_' . $sKey] = $sVal;
+        return $aRet;
+    }
+
+    /**
+     * Checks if mentions enabled for current metatags object
+     */
+    public function mentionsIsEnabled() 
+    {
+        return empty($this->_aObject['table_mentions']) ? false : true;
+    }
+
+    /**
+     * Add \@mentions from the string (most probably \@mentions will be some sort of links already, so parsing may have to look for smth like <a data-mention="bx_persons|123">mention name</a> instead of \@mention, since there is no usernames for profiles modules and name could contain spaces and othr characters)
+     * @param $iId content id
+     * @param $s string with \@mentions
+     * @return number of found mentions 
+     */
+    public function mentionsAdd($iId, $s) 
+    {
+        // TODO:        
+    }
+
+    /**
+     * Add links to the \@mentions in the string (actual tranformation may have to be performed with ready links like <a data-mention="bx_persons|123">mention name</a>)
+     * @param $iId content id
+     * @param $s string with \@mentions
+     * @return modified string where all \@mentions are transformed to links
+     */
+    public function mentionsParse($iId, $s) 
+    {
+        // TODO:
+    }
+
+    /**
+     * No mentions meta info in the head section
+     */
+    protected function mentionsAddMeta($iId) 
+    {
+        return 0;
+    }
+
+    /**
+     * Set condition for search results object for mentions
+     * @param $oSearchResult search results object
+     * @param $sMention smbd
+     */
+    public function mentionsSetSearchCondition($oSearchResult, $sMention)
+    {
+        // TODO:
+    }
+
+    /**
+     * Delete all data associated with the content
+     * @param $iId content id
+     */
+    public function onDeleteContent($iId) 
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'Delete';
+            $i += $this->_oQuery->$sFunc($iId);
+        }
+        return $i;
+    }
+
+}
+
+/** @} */

+ 62 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolMinify.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
+ * @{
+ */
+
+use MatthiasMullie\Minify;
+
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/path-converter/ConverterInterface.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/path-converter/Converter.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/Minify.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/CSS.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/JS.php');
+
+class BxDolMinify extends BxDolFactory implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolMinify();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+	public function minifyJs($s)
+	{
+		$oMinifier = new Minify\JS();
+    	$oMinifier->add($s);
+
+    	return $oMinifier->minify();
+	}
+
+	public function minifyCss($s)
+	{
+		$oMinifier = new Minify\CSS();
+    	$oMinifier->add($s);
+
+    	return $oMinifier->minify();
+	}
+}
+
+/** @} */

+ 293 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolObject.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
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Base class for all "Object" classes.
+ * Child classes usually represents high level programming constructions to generate ready 'objects' functionality, like Comments, Votings, Forms.
+ */
+class BxDolObject extends BxDolFactory implements iBxDolReplaceable
+{
+    protected $_oTemplate = null;
+	protected $_oQuery = null;
+
+    protected $_iId = 0; ///< item id the action to be performed with
+    protected $_sSystem = ''; ///< current system name
+    protected $_aSystem = array(); ///< current system array
+
+    protected $_aMarkers = array ();
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct();
+
+        $aSystems = $this->getSystems();
+        if(!isset($aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $aSystems[$sSystem];
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        if(!$this->isEnabled())
+            return;
+
+        if($iInit)
+            $this->init($iId);
+    }
+
+    public function init($iId)
+    {
+        if(!$this->isEnabled())
+            return false;
+
+        if(empty($this->_iId) && $iId)
+            $this->setId($iId);
+
+		$this->addMarkers(array(
+            'object_id' => $this->getId(),
+            'user_id' => $this->_getAuthorId()
+        ));
+
+        return true;
+    }
+
+    public function getSystemId()
+    {
+        return $this->_aSystem['id'];
+    }
+
+    public function getSystemName()
+    {
+        return $this->_sSystem;
+    }
+
+    public function getSystemInfo()
+    {
+        return $this->_aSystem;
+    }
+
+    public function getId()
+    {
+        return $this->_iId;
+    }
+
+    public function setId($iId)
+    {
+        if($iId == $this->getId())
+            return;
+
+        $this->_iId = $iId;
+    }
+
+    public function isEnabled ()
+    {
+        return $this->_aSystem && (int)$this->_aSystem['is_on'] == 1;
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        return $this->_oQuery->isPerformed($iObjectId, $iAuthorId);        
+    }
+
+	/**
+	 * Interface functions for outer usage
+	 */
+    public function getConditions($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTable = isset($this->_aSystem['table_main']) ? $this->_aSystem['table_main'] : '';
+        if(empty($sTable) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTable,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('count'),
+                ),
+            ),
+        );
+    }
+
+    public function getConditionsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTableTrack = isset($this->_aSystem['table_track']) ? $this->_aSystem['table_track'] : '';
+        if(empty($sTableTrack) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'restriction' => array (
+                'objects_' . $this->_sSystem . '_author' => array(
+                    'value' => $iAuthorId,
+                    'field' => 'author_id',
+                    'operator' => '=',
+                    'table' => $sTableTrack,
+                ),
+            ),
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTableTrack,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('author_id'),
+                ),
+            ),
+
+        );
+    }
+    
+	public function getSqlParts($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlParts($sMainTable, $sMainField);
+    }
+
+    public function getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId);
+    }
+
+    public function addMarkers($aMarkers)
+    {
+        if(empty($aMarkers) || !is_array($aMarkers))
+			return false;
+
+        $this->_aMarkers = array_merge($this->_aMarkers, $aMarkers);
+        return true;
+    }
+
+	/**
+     * Permissions functions
+     */
+	public function checkAction ($sAction, $isPerformAction = false)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system', $isPerformAction);
+        return $a[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkActionErrorMsg ($sAction)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system');
+        return $a[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $a[CHECK_ACTION_MESSAGE] : '';
+    }
+
+	/**
+     * Actions' response functions
+     */
+	public function onObjectDelete($iObjectId = 0)
+    {
+        $this->_oQuery->deleteObjectEntries($iObjectId ? $iObjectId : $this->getId());
+    }
+
+	/**
+     * Internal functions
+     */
+	protected function _getAuthorId ()
+    {
+        return isMember() ? bx_get_logged_profile_id() : 0;
+    }
+
+    protected function _getAuthorPassword ()
+    {
+        return isMember() ? $_COOKIE['memberPassword'] : "";
+    }
+
+    protected function _getAuthorIp ()
+    {
+        return getVisitorIP();
+    }
+
+    protected function _getAuthorInfo($iAuthorId = 0)
+    {
+        $oProfile = $this->_getAuthorObject($iAuthorId);
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit()
+        );
+    }
+
+    protected function _getAuthorObject($iAuthorId = 0)
+    {
+    	if($iAuthorId == 0)
+    		return BxDolProfileUndefined::getInstance();
+
+        $oProfile = BxDolProfile::getInstance($iAuthorId);
+        if(!$oProfile)
+			$oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+    /**
+     * Update Trigger table using data which is automatically gotten from object's internal table.
+     */
+	protected function _trigger()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTable($iId);
+    }
+
+    /**
+     * Update (increment/decrement) Trigger table using provided value.
+     */
+    protected function _triggerValue($iValue)
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTableValue($iId, $iValue);
+    }
+
+    /**
+     * Replace provided markers in a string
+     * @param $mixed string or array to replace markers in
+     * @return string where all occured markers are replaced
+     */
+    protected function _replaceMarkers ($mixed)
+    {
+        return bx_replace_markers($mixed, $this->_aMarkers);
+    }
+}
+
+/** @} */

+ 302 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPayments.php

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

+ 271 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPermalinks.php

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

+ 348 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolPrivacy.php

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

+ 196 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolSession.php

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

+ 1411 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorage.php

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

+ 380 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorageQuery.php

@@ -0,0 +1,380 @@
+<?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->getRow($sQuery);
+    }
+
+    static public function getStorageObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_objects_storage`";
+        return $oDb->getAll($sQuery);
+    }
+
+    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)
+    {
+        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 updateUserQuota($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;
+    }
+
+    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())
+    {
+        $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)
+    {
+        return $this->getFiles($mixedProfileId, true, $iContentId);
+    }
+
+    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)
+    {
+        $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);
+    }
+}
+
+/** @} */

+ 223 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolStorageS3.php

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

+ 2425 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolTemplate.php

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

+ 443 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolUploader.php

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

+ 187 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolView.php

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

+ 81 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolViewQuery.php

@@ -0,0 +1,81 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolView
+ */
+class BxDolViewQuery extends BxDolObjectQuery
+{
+    protected $_iPeriod;
+
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_iPeriod = (int)$aSystem['period'];
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `viewer_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->getOne($sQuery) > 0;
+    }
+
+	public function getPerformedBy($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `viewer_id` FROM `{$this->_sTableTrack}` WHERE `object_id`=? ORDER BY `date` DESC", $iObjectId);
+        return $this->getColumn($sQuery);
+    }
+
+	public function getView($iObjectId)
+    {
+        return array('count' => $this->getObjectCount($iObjectId));
+    }
+
+    public function doView($iObjectId, $iAuthorId, $sAuthorIp)
+    {
+        $iAuthorNip = ip2long($sAuthorIp);
+
+        $aBindings = array(
+        	'object_id' => $iObjectId
+        );
+        if($iAuthorId) {
+        	$aBindings['viewer_id'] = $iAuthorId;
+
+            $sWhere = " AND `viewer_id` = :viewer_id ";
+        }
+        else {
+        	$aBindings['viewer_nip'] = $iAuthorNip;
+
+            $sWhere = " AND `viewer_id` = '0' AND `viewer_nip` = :viewer_nip ";
+        }
+
+        $iDate = (int)$this->getOne("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = :object_id " . $sWhere, $aBindings);
+        $iDateNow = time();
+
+        if(!$iDate) {
+            $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `viewer_id` = ?, `viewer_nip` = ?, `date` = ?", $iObjectId, $iAuthorId, $iAuthorNip, $iDateNow);
+            return (int)$this->query($sQuery) > 0;
+        }
+
+        if(($iDateNow - $iDate) > $this->_iPeriod) {
+            $sQuery = $this->prepare("UPDATE `{$this->_sTableTrack}` SET `date` = ? WHERE `object_id` = ? AND `viewer_id` = ? AND `viewer_nip` = ?", $iDateNow, $iObjectId, $iAuthorId, $iAuthorNip);
+            return (int)$this->query($sQuery) > 0;
+        }
+    }
+
+    public function updateTriggerTable($iObjectId)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = `{$this->_sTriggerFieldCount}` + 1 WHERE `{$this->_sTriggerFieldId}` = ?", $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+}
+
+/** @} */

+ 317 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/classes/BxDolVote.php

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

+ 116 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolFeature.js

@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolFeature(options)
+{
+	this._sObjName = undefined == options.sObjName ? 'oFeature' : 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 = 'feature.php';
+    this._sActionsUrl = options.sRootUrl + this._sActionsUri; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+    this._sSP = undefined == options.sStylePrefix ? 'bx-feature' : options.sStylePrefix;
+    this._aHtmlIds = options.aHtmlIds;
+
+    this._oParent = null;
+}
+
+BxDolFeature.prototype.feature = function(oLink) {
+	var $this = this;
+    var oData = this._getDefaultParams();
+    oData['action'] = 'Feature';
+
+    this._oParent = oLink;
+
+    $.get(
+    	this._sActionsUrl,
+    	oData,
+    	function(oData) {
+    		$this.processJson(oData, oLink);
+    	},
+    	'json'
+    );
+};
+
+BxDolFeature.prototype.onFeature = function(oData, oElement)
+{
+	if(oData && oData.msg != undefined && oData.msg.length > 0)
+        alert(oData.msg);
+
+	if(oData && oData.code != 0)
+        return;
+
+	if(oData && oData.label_icon)
+		$(oElement).find('.sys-icon').attr('class', 'sys-icon ' + oData.label_icon);
+
+	if(oData && oData.label_title) {
+		$(oElement).attr('title', oData.label_title);
+		$(oElement).find('span').html(oData.label_title);
+	}
+
+	if(oData && oData.disabled)
+		$(oElement).removeAttr('onclick').addClass($(oElement).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-feature-disabled');
+};
+
+BxDolFeature.prototype.processJson = function(oData, oElement) {
+	oElement = oElement != undefined ? oElement : this._oParent;
+
+    //--- Show Message
+    if(oData && oData.msg != undefined)
+        alert(oData.msg);
+    if(oData && oData.message != undefined)
+        alert(oData.message);
+
+    //--- Show Popup
+    if(oData && oData.popup != undefined) {
+    	$('#' + oData.popup_id).remove();
+
+    	$(oData.popup).hide().prependTo('body').dolPopup({
+    		pointer: {
+    			el: oElement
+    		},
+            fog: {
+				color: '#fff',
+				opacity: .7
+            }
+        });
+    }
+
+    //--- Evaluate JS code
+    if (oData && oData.eval != undefined)
+        eval(oData.eval);
+};
+
+BxDolFeature.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');
+};
+
+BxDolFeature.prototype._loadingInButton = function(e, bShow) {
+	if($(e).length)
+		bx_loading_btn($(e), bShow);
+	else
+		bx_loading($('body'), bShow);	
+};
+
+BxDolFeature.prototype._getDefaultParams = function() {
+	var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        object_id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 52 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolView.js

@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolView(options)
+{
+	this._sObjName = undefined == options.sObjName ? 'oView' : 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 = 'view.php';
+    this._sActionsUrl = options.sRootUrl + this._sActionsUri; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+    this._sSP = undefined == options.sStylePrefix ? 'bx-view' : options.sStylePrefix;
+    this._aHtmlIds = options.aHtmlIds;
+}
+
+BxDolView.prototype.toggleByPopup = function(oLink) {
+	var $this = this;
+    var oData = this._getDefaultParams();
+    oData['action'] = 'GetViewedBy';
+
+	$(oLink).dolPopupAjax({
+		id: this._aHtmlIds['by_popup'], 
+		url: bx_append_url_params(this._sActionsUri, oData)
+	});
+};
+
+BxDolView.prototype._loadingInButton = function(e, bShow) {
+	if($(e).length)
+		bx_loading_btn($(e), bShow);
+	else
+		bx_loading($('body'), bShow);	
+};
+
+BxDolView.prototype._getDefaultParams = function() {
+	var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 176 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/classes/BxDolVote.js

@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolVote(options)
+{
+	this._sObjName = undefined == options.sObjName ? 'oVote' : 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._iLikeMode = undefined == options.iLikeMode ? 0 : options.iLikeMode;
+
+    this._sActionsUri = 'vote.php';
+    this._sActionsUrl = options.sRootUrl + this._sActionsUri; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+    this._sSP = undefined == options.sStylePrefix ? 'bx-vote' : options.sStylePrefix;
+    this._aHtmlIds = options.aHtmlIds;
+
+    this._iSaveWidth = -1;
+
+    //--- Init stars based vote ---//
+    var $this = this;
+    if(!this._iLikeMode) {
+    	$('.' + this._sSP + '.' + this._sSP + '-stars').each(function() {
+    		var oDoVote = $(this);
+	    	var fRate = oDoVote.attr('bx_vote_data_rate');
+	    	var iStarWidth = $this._getStarWidthDo(oDoVote);
+
+	    	$this._getSliderDo(oDoVote).width(Math.round(fRate * iStarWidth));
+    	});
+
+    	$('.' + this._sSP + '-legend.' + this._sSP + '-legend-stars').each(function() {
+    		var oLegend = $(this);
+    		var iStarWidth = $this._getStarWidthLegend(oLegend);
+
+	    	oLegend.find('.' + $this._sSP + '-legend-item').each(function() {
+	    		var oItem = $(this);
+
+	    		oItem.find('.' + $this._sSP + '-slider').width(parseInt(oItem.attr('bx_vote_item_value')) * iStarWidth); 
+	    	});
+    	});
+    }
+}
+
+BxDolVote.prototype.toggleByPopup = function(oLink) {
+	var $this = this;
+    var oData = this._getDefaultParams();
+    oData['action'] = 'GetVotedBy';
+
+	$(oLink).dolPopupAjax({
+		id: this._aHtmlIds['by_popup'], 
+		url: bx_append_url_params(this._sActionsUri, oData)
+	});
+};
+
+BxDolVote.prototype.over = function (oLink)
+{
+	var oSlider = this._getSliderDo(oLink);
+	var iIndex = this._getButtons(oLink).index(oLink);
+
+    this._iSaveWidth = parseInt(oSlider.width());
+    oSlider.width((iIndex + 1) * this._getStarWidthDo(oLink));
+};
+
+BxDolVote.prototype.out = function (oLink)
+{
+	var oSlider = this._getSliderDo(oLink);
+
+	oSlider.width(this._iSaveWidth);
+};
+
+BxDolVote.prototype.vote = function (oLink, iValue, onComplete)
+{
+    var $this = this;
+    var oParams = this._getDefaultParams();
+    oParams['action'] = 'Vote';
+    oParams['value'] = iValue;
+
+    $.post(
+    	this._sActionsUrl,
+    	oParams,
+    	function(oData) {
+    		if(oData && oData.msg != undefined)
+                alert(oData.msg);
+
+    		if(oData && oData.code != 0)
+                return;
+
+    		if($this._iLikeMode) {
+	    		if(oData && oData.label_icon)
+	    			$(oLink).find('.sys-icon').attr('class', 'sys-icon ' + oData.label_icon);
+
+	    		if(oData && oData.label_title) {
+	    			$(oLink).attr('title', oData.label_title);
+	    			$(oLink).find('span').html(oData.label_title);
+	    		}
+
+	    		if(oData && oData.disabled)
+	    			$(oLink).removeAttr('onclick').addClass($(oLink).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-vote-disabled');
+    		}
+
+    		if(!$this._iLikeMode)
+    			$this._iSaveWidth = Math.round(oData.rate * $this._getStarWidthDo(oLink));
+
+            var oCounter = $this._getCounter(oLink);
+            if(oCounter && oCounter.length > 0) {
+            	oCounter.html(oData.countf);
+
+            	oCounter.parents('.' + $this._sSP + '-counter-holder:first').bx_anim(oData.count > 0 ? 'show' : 'hide');
+            }
+
+            if(typeof onComplete == 'function')
+            	onComplete(oLink, oData);
+        },
+        'json'
+    );
+};
+
+BxDolVote.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');
+};
+
+BxDolVote.prototype._getSliderDo = function(oElement) {
+	return this._getSlider(oElement, '.' + this._sSP + '-do');
+};
+BxDolVote.prototype._getSliderLegend = function(oElement) {
+	return this._getSlider(oElement, '.' + this._sSP + '-legend');
+};
+BxDolVote.prototype._getSlider = function(oElement, sParent) {
+	var sSlider = (sParent.length > 0 ? sParent + ' ' : '') + '.' + this._sSP + '-slider';
+	if($(oElement).hasClass(this._sSP))
+		return $(oElement).find(sSlider);
+	else
+		return $(oElement).parents('.' + this._sSP + ':first').find(sSlider);
+};
+
+BxDolVote.prototype._getStarWidthDo = function(oElement) {
+	return this._getSliderDo(oElement).find('.sys-icon').width();
+};
+BxDolVote.prototype._getStarWidthLegend = function(oElement) {
+	return this._getSliderLegend(oElement).find('.sys-icon').width();
+};
+
+BxDolVote.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');
+};
+
+BxDolVote.prototype._loadingInButton = function(e, bShow) {
+	if($(e).length)
+		bx_loading_btn($(e), bShow);
+	else
+		bx_loading($('body'), bShow);	
+};
+
+BxDolVote.prototype._getDefaultParams = function() {
+	var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 733 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/functions.js

@@ -0,0 +1,733 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function processJsonData(oData) {
+	var $this = this;
+
+	if(oData && oData.message != undefined && oData.message.length != 0)
+    	alert(oData.message);
+
+    if(oData && oData.reload != undefined && parseInt(oData.reload) == 1)
+    	document.location = document.location;
+
+    if(oData && oData.popup != undefined) {
+    	var oPopup = null;
+    	var oOptions = {
+            fog: {
+				color: '#fff',
+				opacity: .7
+            },
+            closeOnOuterClick: false
+        };
+
+    	if(typeof(oData.popup) == 'object') {
+    		oOptions = $.extend({}, oOptions, oData.popup.options);
+    		oPopup = $(oData.popup.html);
+    	}
+    	else 
+    		oPopup = $(oData.popup);
+
+    	$('#' + oPopup.attr('id')).remove();
+        oPopup.hide().prependTo('body').dolPopup(oOptions);
+    }
+
+    if (oData && oData.eval != undefined)
+        eval(oData.eval);
+};
+
+function getHtmlData( elem, url, callback, method , confirmation)
+{
+    if ('undefined' != typeof(confirmation) && confirmation && !confirm(_t('_are you sure?'))) 
+        return false;
+
+    // in most cases it is element ID, in other cases - object of jQuery
+    if (typeof elem == 'string')
+        elem = '#' + elem; // create selector from ID
+
+    var $block = $(elem);
+
+    var blockPos = $block.css('position');
+
+    $block.css('position', 'relative'); // set temporarily for displaying "loading icon"
+
+    bx_loading_content($block, true);
+    var $loadingDiv = $block.find('.bx-loading-ajax');
+
+    var iLeftOff = parseInt(($block.innerWidth() / 2.0) - ($loadingDiv.outerWidth()  / 2.0));
+    var iTopOff  = parseInt(($block.innerHeight() / 2.0) - ($loadingDiv.outerHeight()));
+    if (iTopOff<0) iTopOff = 0;
+
+    $loadingDiv.css({
+        position: 'absolute',
+        left: iLeftOff,
+        top:  iTopOff
+    });
+
+    if (undefined != method && (method == 'post' || method == 'POST')) {
+
+        $.post(url, function(data) {
+
+            $block.html(data);
+	        $block.css('position', blockPos).bxTime();
+            if ($.isFunction($.fn.addWebForms))
+                $block.addWebForms();
+
+            bx_activate_anim_icons();
+
+            if (typeof callback == 'function')
+                callback.apply($block);
+        });
+
+    } else {
+
+        $block.load(url + '&_r=' + Math.random(), function() {
+
+	        $(this).css('position', blockPos).bxTime();
+            if ($.isFunction($.fn.addWebForms))
+                $(this).addWebForms();
+
+            bx_activate_anim_icons();
+
+            if (typeof callback == 'function')
+                callback.apply(this);
+        });
+
+    }
+}
+
+function loadDynamicBlockAutoPaginate (e, iStart, iPerPage, sAdditionalUrlParams) {
+
+    sUrl = location.href;
+
+    sUrl = sUrl.replace(/start=\d+/, '').replace(/per_page=\d+/, '').replace(/[&\?]+$/, '');
+    sUrl = bx_append_url_params(sUrl, 'start=' + parseInt(iStart) + '&per_page=' + parseInt(iPerPage));
+    if ('undefined' != typeof(sAdditionalUrlParams))
+        sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams);
+
+    return loadDynamicBlockAuto(e, sUrl);
+}
+
+/**
+ * This function reloads page block automatically, 
+ * just provide any element inside the block and this function will reload the block.
+ * @param e - element inside the block
+ * @return true on success, or false on error - particularly, if block isn't found
+ */
+function loadDynamicBlockAuto(e, sUrl) {
+	var oContainer = $(e).parents('.bx-page-block-container:first');
+    var sId = oContainer.attr('id');
+
+    if ('undefined' == typeof(sUrl))
+        sUrl = location.href;
+
+    if (!sId || !sId.length)
+        return false;
+
+    var aMatches = sId.match(/\-(\d+)$/);
+    if (!aMatches || !aMatches[1])
+        return false;
+
+    var oPaginate = oContainer.find('.bx-paginate'); 
+    if(oPaginate.length > 0) {
+    	var iStart = parseInt(oPaginate.attr('bx-data-start'));
+    	var iPerPage = parseInt(oPaginate.attr('bx-data-perpage'));
+    	if(!isNaN(iStart) && !isNaN(iPerPage) && sUrl.indexOf('start') == -1 && sUrl.indexOf('per_page') == -1)
+    		sUrl = bx_append_url_params(sUrl, 'start=' + iStart + '&per_page=' + iPerPage);
+    }
+
+    loadDynamicBlock(parseInt(aMatches[1]), sUrl);
+    return true;
+}
+
+function loadDynamicBlock(iBlockID, sUrl) {
+    getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID));
+    return true;
+}
+
+function loadDynamicPopupBlock(iBlockID, sUrl) {
+    if (!$('#dynamicPopup').length) {
+        $('<div id="dynamicPopup" style="display:none;"></div>').prependTo('body');
+    }
+
+    $('#dynamicPopup').load(
+        (sUrl + '&dynamic=popup&pageBlock=' + iBlockID),
+        function() {
+            $(this).dolPopup({
+                left: 0,
+                top: 0
+            });
+        }
+    );
+}
+
+function closeDynamicPopupBlock() {
+    $('#dynamicPopup').dolPopupHide();
+}
+
+
+/**
+ * Translate string
+ */
+function _t(s, arg0, arg1, arg2) {
+    if (!window.aDolLang || !aDolLang[s])
+        return s;
+
+    cs = aDolLang[s];
+    cs = cs.replace(/\{0\}/g, arg0);
+    cs = cs.replace(/\{1\}/g, arg1);
+    cs = cs.replace(/\{2\}/g, arg2);
+    return cs;
+}
+
+function showPopupAnyHtml(sUrl, sId) {
+
+    var oPopupOptions = {};
+
+    if (!sId || !sId.length)
+        sId = 'login_div';
+
+    $('#' + sId).remove();
+    $('<div id="' + sId + '" style="display: none;"></div>').prependTo('body').load(
+        sUrl.match('^http[s]{0,1}:\/\/') ? sUrl : sUrlRoot + sUrl,
+        function() {
+            $(this).dolPopup(oPopupOptions);
+        }
+    );
+}
+
+function bx_loading_svg(sType, sClass) {
+	sClass = sClass != undefined && sClass.length > 0 ? sClass : '';
+
+	var s = '';
+	s += '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 520 520" class="' + sClass + '" style="enable-background:new 0 0 520 520;" xml:space="preserve">';
+	s += '<g class="' + sType + '">';
+	s += '<g class="inner">';
+	s += '<path class="p1" d="M152,260C152,260,152,260,152,260c0-59.8,48.4-108.4,108.1-108.4c43.3,0,80.9,25.6,97.9,62.4V107.3c-28-18.3-61.9-28.9-98.1-28.9C159.8,78.4,78.6,159.9,78.6,260c0,59.5,28.4,112.4,73.4,145.6V260z"/>';
+	s += '<path class="p2" d="M368,114.4V260c0,59.8-48.4,108.4-108.1,108.4c-43.3,0-80.9-25.6-97.9-62.4v106.7c28,18.3,61.9,28.9,98.1,28.9c100.1,0,181.3-81.4,181.3-181.6C441.4,200.5,413,147.6,368,114.4z"/>';
+	s += '</g>';
+	s += '<g class="outer">';
+	s += '<path class="p1" d="M146.9,106.7c-8.1-15-36.9-29.9-68.9-27.3v124.2C90,164.3,114.6,130.5,146.9,106.7z"/>';
+	s += '<path class="p2" d="M373.1,413.3c8.1,15,36.7,29.9,68.9,27.3V316.4C429.8,355.7,405.4,389.5,373.1,413.3z"/>';
+	s += '</g>';
+	s += '</g>';
+	s += '</svg>';
+	return s;
+}
+
+function bx_loading_animate (e, aOptions) {
+    e = $(e);
+    if (!e.length)
+        return false;
+    if (e.find('.bx-sys-spinner').length)
+        return false;
+    return new Spinner(aOptions).spin(e.get(0));
+}
+
+function bx_loading_btn (oElement, bEnable) {
+	var bUseSvg = true;
+
+    var oButton = $(oElement);
+
+    if (oButton.children('div').size())
+        oButton = oButton.children('div').first();
+
+    if(!bEnable)
+    	oButton.find('.bx-loading-ajax-btn').remove();
+    else if (!oButton.find('.bx-loading-ajax-btn').length)
+    	oButton.append('<b class="bx-loading-ajax-btn bx-btn-height">' + (bUseSvg ? bx_loading_svg('colored', 'bx-btn-height') : '') + '</b>');
+
+    if(!bUseSvg)
+    	bx_loading_animate(oButton.find('.bx-loading-ajax-btn'), aSpinnerSmallOpts);    
+}
+
+function bx_loading_content (oElement, bEnable, isReplace) {
+	var bUseSvg = true;
+
+    var oBlock = $(oElement);
+    var oLoading = $('<div class="bx-loading-ajax bx-def-z-index-front">' + (bUseSvg ? bx_loading_svg('colored') : '') + '</div>');
+    
+    if(!bEnable)
+    	oBlock.find(".bx-loading-ajax").remove();
+    else if(!oBlock.find('.bx-loading-ajax').length) {
+        if('undefined' != typeof(isReplace) && isReplace)
+            oBlock.html(oLoading.addClass('static'));
+        else
+            oBlock.append(oLoading);
+
+        if(!bUseSvg)
+        	bx_loading_animate(oBlock.find('.bx-loading-ajax'), aSpinnerOpts);
+    } 
+}
+
+function bx_loading (elem, b) {
+
+    if (typeof elem == 'string')
+        elem = '#' + elem;
+
+    var block = $(elem);
+
+    if (block.hasClass('bx-btn')) {
+        bx_loading_btn (block, b);
+        return;
+    }
+
+    if (1 == b || true == b) {
+
+        bx_loading_content(block, b);
+
+        e = block.find(".bx-loading-ajax");
+        e.css('left', parseInt(block.width()/2.0 - e.width()/2.0));
+
+        var he = e.outerHeight();
+        var hc = block.outerHeight();
+
+        if (block.css('position') != 'relative' && block.css('position') != 'absolute') {
+            if (!block.data('css-save-position'))
+                block.data('css-save-position', block.css('position'));
+            block.css('position', 'relative');
+        }
+
+        if (hc > he) {
+            e.css('top', parseInt(hc/2.0 - he/2.0));
+        }
+
+        if (hc < he) {
+            if (!block.data('css-save-min-height'))
+                block.data('css-save-min-height', block.css('min-height'));
+            block.css('min-height', he);
+        }
+
+    } else {
+
+        if (block.data('css-save-position'))
+            block.css('position', block.data('css-save-position'));
+
+        if (block.data('css-save-min-height'))
+            block.css('min-height', block.data('css-save-min-height'));
+
+        bx_loading_content(block, b);
+
+    }
+
+}
+
+
+/**
+ * Center content with floating blocks.
+ * sSel - jQuery selector of content to be centered
+ * sBlockSel - jquery selector of blocks
+ */
+function bx_center_content (sSel, sBlockStyle, bListenOnResize) {
+
+    var sId;
+    if ($(sSel).parent().hasClass('bx-center-content-wrapper')) {
+        sId = $(sSel).parent().attr('id');
+    } else {
+        sId = 'id' + (new Date()).getTime();
+        $(sSel).wrap('<div id="'+sId+'" class="bx-center-content-wrapper"></div>');
+    }
+
+    var eCenter = $('#' + sId);
+    var iAll = $('#' + sId + ' ' + sBlockStyle).size();
+    var iWidthUnit = $('#' + sId + ' ' + sBlockStyle + ':first').outerWidth(true);
+    var iWidthContainer = eCenter.innerWidth();           
+    var iPerRow = parseInt(iWidthContainer/iWidthUnit);
+    var iLeft = (iWidthContainer - (iAll > iPerRow ? iPerRow * iWidthUnit : iAll * iWidthUnit)) / 2;
+
+    if (iWidthUnit > iWidthContainer)
+        return;
+
+    if ('undefined' != typeof(bListenOnResize) && bListenOnResize) {
+        $(window).on('resize.bx-center-content', function () {
+            bx_center_content(sSel, sBlockStyle);
+        });
+    }
+
+    eCenter.css("padding-left", iLeft);
+}
+
+/**
+ * Show pointer popup with menu from URL.
+ * @param o - menu object name
+ * @param e - element to show popup at
+ * @param options - popup options
+ * @param vars - additional GET variables
+ */
+function bx_menu_popup (o, e, options, vars) {
+    var options = options || {};
+    var vars = vars || {};
+    var o = $.extend({}, $.fn.dolPopupDefaultOptions, {id: o, url: bx_append_url_params('menu.php', $.extend({o:o}, vars))}, options);
+    if ('undefined' == typeof(e))
+        e = window;
+    $(e).dolPopupAjax(o);
+}
+
+/**
+ * Show pointer popup with menu from existing HTML.
+ * @param jSel - jQuery selector for html to show in popup
+ * @param e - element to show popup at
+ * @param options - popup options
+ */
+function bx_menu_popup_inline (jSel, e, options) {
+    var options = options || {};
+    var o = $.extend({}, $.fn.dolPopupDefaultOptions, options, {pointer:{el:$(e)}});
+    if ($(jSel + ':visible').length) 
+        $(jSel).dolPopupHide(); 
+    else 
+        $(jSel).dolPopup(o);
+}
+
+/**
+ * Show pointer popup with menu from existing HTML.
+ * @param jSel - jQuery selector for html to show in popup
+ * @param e - element to click to open/close slider
+ * @param sPosition - 'block' for sliding menu in blocks, 'site' - for sliding main menu
+ */
+function bx_menu_slide (jSel, e, sPosition) {
+    var options = options || {};
+    var eSlider = $(jSel);    
+
+    if ('undefined' == typeof(e))
+        e = eSlider.data('data-control-btn');
+
+    var eIcon = $(e).find('.sys-icon-a');    
+
+    var fPositionBlock = function () {
+        var eBlock = eSlider.parents('.bx-page-block-container');
+        eSlider.css({
+            position: 'absolute',
+            top: eBlock.find('.bx-db-header').outerHeight(true),
+            left: 0
+            //width: eBlock.width() TODO: Commited to test. Remove if everything is OK.
+        });
+    };
+
+    var fPositionSite = function () {
+        var eToolbar = $('#bx-toolbar');
+        eSlider.css({
+            position: 'fixed',
+            top: eToolbar.outerHeight(true),
+            left: 0
+        });
+    };
+
+    var fClose = function () {
+        if (eIcon.length)
+            (new Marka(eIcon[0])).set(eIcon.attr('data-icon-orig'));
+        eSlider.slideUp()
+    };
+
+    var fOpen = function () {
+        if (eIcon.length) {
+            eIcon.attr('data-icon-orig', eIcon.attr('data-icon'));
+            (new Marka(eIcon[0])).set('times');
+        }
+
+        if ('block' == sPosition)
+            fPositionBlock();
+        else
+            fPositionSite();
+        
+        eSlider.slideDown();
+
+        eSlider.data('data-control-btn', e);        
+    };    
+    
+    if ('undefined' == typeof(sPosition))
+        sPosition = 'block';
+
+    if ($(jSel + ':visible').length) {
+        fClose();
+
+        $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu');
+        $(window).off('resize.bx-sliding-menu');
+    }
+    else {
+        bx_menu_slide_close_all_opened();
+
+        $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu');
+        $(window).off('resize.bx-sliding-menu');
+
+        fOpen();
+        eSlider.find('a').each(function () {
+            $(this).off('click.bx-sliding-menu');
+            $(this).on('click.bx-sliding-menu', function () {
+                fClose();
+            });
+        });
+
+        setTimeout(function () {
+
+            var iWidthPrev = $(window).width();
+            $(window).on('resize.bx-sliding-menu', function () {
+                if ($(this).width() == iWidthPrev)
+                    return;
+                iWidthPrev = $(this).width();
+                bx_menu_slide_close_all_opened();
+            });
+ 
+            $(document).on('click.bx-sliding-menu touchend.bx-sliding-menu', function (event) {
+                if ($(event.target).parents('.bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || $(event.target).filter('.bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || e === event.target)
+                    event.stopPropagation();
+                else
+                    bx_menu_slide_close_all_opened();
+            });
+
+        }, 10);
+    }
+}
+
+/**
+ * Close all opened sliding menus
+ */
+function bx_menu_slide_close_all_opened () {
+    $('.bx-sliding-menu-main:visible, .bx-popup-slide-wrapper:visible').each(function () {
+        bx_menu_slide('#' + this.id);
+    });
+}
+
+/**
+ * Set ACL level for specified profile
+ * @param iProfileId - profile id to set acl level for
+ * @param iAclLevel - acl level id to assign to a given rofile
+ */
+function bx_set_acl_level (iProfileId, iAclLevel, mixedLoadingElement) {
+
+    if ('undefined' != typeof(mixedLoadingElement))
+        bx_loading($(mixedLoadingElement), true);
+
+    $.post(sUrlRoot + 'menu.php', {o:'sys_set_acl_level', 'profile_id': iProfileId, 'acl_level_id': iAclLevel}, function(data) {
+
+        if ('undefined' != typeof(mixedLoadingElement))
+            bx_loading($(mixedLoadingElement), false);
+
+        if (data.length) {
+            alert(data);
+        } else if ($(mixedLoadingElement).hasClass('bx-popup-applied')) {
+            $(mixedLoadingElement).dolPopupHide().remove();
+        }
+    });
+}
+
+function validateLoginForm(eForm) {
+    return true;
+}
+
+/**
+ * convert <time> tags to human readable format
+ * @param sLang - use sLang localization
+ * @param isAutoupdate - for internal usage only
+ */
+function bx_time(sLang, isAutoupdate, sRootSel) {
+    if ('undefined' == typeof(sRootSel))
+        sRootSel = 'body';
+    var iAutoupdate = 22*60*60; // autoupdate time in realtime if less than 22 hours
+    var sSel = 'time';
+
+    if ('undefined' == typeof(sLang)) {
+        sLang = glBxTimeLang;
+    } else {
+        glBxTimeLang = sLang;
+    }
+
+    if ('undefined' != typeof(isAutoupdate) && isAutoupdate)
+        sSel += '.bx-time-autoupdate';
+
+    $(sRootSel).find(sSel).each(function () {
+        var s;
+        var sTime = $(this).attr('datetime');
+        var iSecondsDiff = moment(sTime).unix() - moment().unix();
+        if (iSecondsDiff < 0)
+            iSecondsDiff = -iSecondsDiff;
+
+        if (iSecondsDiff < $(this).attr('data-bx-autoformat'))
+            s = moment(sTime).locale(sLang).fromNow(); // 'time ago' format
+        else
+            s = moment(sTime).locale(sLang).format($(this).attr('data-bx-format')); // custom format
+
+        if (iSecondsDiff < iAutoupdate)
+            $(this).addClass('bx-time-autoupdate');
+        else
+            $(this).removeClass('bx-time-autoupdate');
+
+        $(this).html(s);
+    });
+
+    if ($('time.bx-time-autoupdate').size()) {
+        setTimeout(function () {
+            bx_time(sLang, true);
+        }, 20000);
+    }
+}
+
+(function($) {
+    $.fn.bxTime = function() {
+        bx_time(undefined, undefined, this);
+        return this;
+    }; 
+} (jQuery));
+
+
+/**
+ * Perform connections AJAX request. 
+ * In case of error - it shows js alert with error message.
+ * In case of success - the page is reloaded.
+ * 
+ * @param sObj - connections object
+ * @param sAction - 'add', 'remove' or 'reject'
+ * @param iContentId - content id, initiator is always current logged in user
+ * @param bConfirm - show confirmation dialog
+ */
+function bx_conn_action(e, sObj, sAction, iContentId, bConfirm, fOnComplete) {
+    if ('undefined' != typeof(bConfirm) && bConfirm && !confirm(_t('_are you sure?')))
+        return;
+                
+    var aParams = {
+        obj: sObj,
+        act: sAction,
+        id: iContentId
+    };
+    var fCallback = function (data) {
+        bx_loading_btn(e, 0);
+        if ('object' != typeof(data))
+            return;
+        if (data.err) {
+            alert(data.msg);
+        } else {
+            if ('function' == typeof(fOnComplete))
+                fOnComplete(data, e);
+            else if (!loadDynamicBlockAuto(e))
+                location.reload();
+            else
+                $('#bx-popup-ajax-wrapper-bx_persons_view_actions_more').remove();
+        }
+    };
+
+    bx_loading_btn(e, 1);
+
+    $.ajax({
+        dataType: 'json',
+        url: sUrlRoot + 'conn.php',
+        data: aParams,
+        type: 'POST',
+        success: fCallback
+    });
+}
+
+function bx_append_url_params (sUrl, mixedParams) {
+    var sParams = sUrl.indexOf('?') == -1 ? '?' : '&';
+
+    if(mixedParams instanceof Array) {
+    	for(var i in mixedParams)
+            sParams += i + '=' + mixedParams[i] + '&';
+        sParams = sParams.substr(0, sParams.length-1);
+    }
+    else if(mixedParams instanceof Object) {
+    	$.each(mixedParams, function(sKey, sValue) {
+    		sParams += sKey + '=' + sValue + '&';
+    	});
+        sParams = sParams.substr(0, sParams.length-1);
+    }
+    else
+        sParams += mixedParams;
+
+    return sUrl + sParams;
+}
+
+function bx_search_on_type (e, n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults) {
+    if ('undefined' != typeof(e) && 13 == e.keyCode) {
+        $(sFormSel).find('input[name=live_search]').val(0);
+        $(sFormSel).submit();
+        return false;
+    }
+
+    if ('undefined' != typeof(glBxSearchTimeoutHandler) && glBxSearchTimeoutHandler)
+        clearTimeout(glBxSearchTimeoutHandler);
+
+    glBxSearchTimeoutHandler = setTimeout(function () {
+        bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults);
+    }, 500);
+
+    return true;
+}
+
+function bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults) {
+    if ('undefined' == typeof(sLoadingContSel))
+        sLoadingContSel = sResultsContSel;
+    if ('undefined' == typeof(bSortResults))
+        bSortResults = false;
+
+    var sQuery = $('input', sFormSel).serialize();
+
+    bx_loading($(sLoadingContSel), true);
+    $.post(sUrlRoot + 'searchKeywordContent.php', sQuery, function(data) {
+        bx_loading($(sLoadingContSel), false);
+        if (bSortResults) {
+            var aSortedUnits = $(data).find(".bx-def-unit-live-search").toArray().sort(function (a, b) {
+                return b.getAttribute('data-ts') - a.getAttribute('data-ts');
+            });
+            data = '';
+            $.each(aSortedUnits.slice(0, n), function (i, e) {
+                data += e.outerHTML;
+            });
+        } 
+        $(sResultsContSel).html(data).bxTime();
+    });
+
+    return false;
+}
+
+function on_filter_apply(e, sInputId, sFilterName)
+{
+	var oRegExp = new RegExp("[&]{0,1}" + sFilterName + "=.*");
+    var s = ('' + document.location).replace(oRegExp, ''); // remove filter
+    s = s.replace(/page=\d+/, 'page=1'); // goto 1st page
+    if (e.checked && $('#' + sInputId).val().length > 2)
+        s += (-1 == s.indexOf('?') ? '?' : '&') + sFilterName + '=' + $('#' + sInputId).val(); // append filter
+    document.location = s;
+}
+
+function on_filter_key_up (e, sCheckboxId)
+{
+    if (13 == e.keyCode) {
+        $('#' + sCheckboxId).click();
+        return false;
+    }
+    else {
+        $('#' + sCheckboxId).removeAttr('checked');
+        return true;
+    }
+}
+
+function bx_activate_anim_icons(sColor)
+{
+    if ('undefined' == typeof(sColor))
+        sColor = glBxAnimIconColor;
+    else
+       glBxAnimIconColor = sColor;
+
+    $('.sys-icon-a').not('.marka').each(function () {
+        var e = $(this);
+        var m = new Marka(e.get(0)), r = e.attr('data-rotate'), c = e.attr('data-color');
+        m.set(e.attr('data-icon')).rotate(r ? r : 'down').color(c ? c : sColor);
+    });
+}
+
+function bx_get_style(sUrl)
+{
+	$("head").append($("<link rel='stylesheet' href='" + sUrl + "' type='text/css' />"));
+}
+
+function bx_get_param(s) {
+    if (!window.aDolOptions || !aDolOptions[s])
+        return false;
+
+    return aDolOptions[s];
+}
+
+/** @} */

+ 23 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/jquery.dolEmbedly.js

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

+ 582 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/js/jquery.dolPopup.js

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

+ 284 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/profiles.inc.php

@@ -0,0 +1,284 @@
+<?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() + 24*60*60*30 : 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'
+    );
+
+    $sID = isset($_COOKIE['memberID']) ? bx_process_input($_COOKIE['memberID']) : false;
+    $sPassword = isset($_COOKIE['memberPassword']) ? bx_process_input($_COOKIE['memberPassword']) : false;
+
+    $bLogged = false;
+    foreach ($aAccTypes as $iRole => $sValue) {
+        if ($GLOBALS['logged'][$sValue] = ($sID && !bx_check_login($sID, $sPassword, $iRole))) {
+            $bLogged = true;
+            break;
+        }
+    }
+
+    if ((isset($_COOKIE['memberID']) || isset($_COOKIE['memberPassword'])) && !$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");
+    }
+
+    $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))
+        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)
+{
+    $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);
+    }
+
+    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();
+
+/** @} */

+ 1522 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/utils.inc.php

@@ -0,0 +1,1522 @@
+<?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
+ *      @encode
+ * @param $mixedParams optional parameters to pass for validation
+ * @param $isCheckMagicQuotes deprecated
+ * @return the filtered data, or FALSE if the filter fails.
+ */
+function bx_process_input ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false, $isCheckMagicQuotes = true)
+{
+    if (is_array($mixedData)) {
+        foreach ($mixedData as $k => $v)
+            $mixedData[$k] = bx_process_input($v, $iDataType, $mixedParams);
+        return $mixedData;
+    }
+
+    if (get_magic_quotes_gpc() && $isCheckMagicQuotes)
+        $mixedData = stripslashes($mixedData);
+
+    switch ($iDataType) {
+    case BX_DATA_INT:
+        return filter_var(trim($mixedData), FILTER_VALIDATE_INT);
+    case BX_DATA_FLOAT:
+        return filter_var(trim($mixedData), FILTER_VALIDATE_FLOAT);
+    case BX_DATA_CHECKBOX:
+        return 'on' == trim($mixedData) ? 'on' : '';
+
+    case BX_DATA_DATE:
+        // maybe consider using strtotime
+        $mixedData = trim($mixedData);
+        if (!preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $mixedData))
+            return false;
+        list($iYear, $iMonth, $iDay) = explode('-', $mixedData); // 1985-10-28
+        $iDay   = intval($iDay);
+        $iMonth = intval($iMonth);
+        $iYear  = intval($iYear);
+        return sprintf("%04d-%02d-%02d", $iYear, $iMonth, $iDay);
+    case BX_DATA_DATETIME:
+        // maybe consider using strtotime
+        $mixedData = trim($mixedData);
+        if (!preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+):(\d+)#', $mixedData, $m) && !preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+)#', $mixedData, $m))
+            return bx_process_input ($mixedData, BX_DATA_DATE, $mixedParams, $isCheckMagicQuotes);
+        $iDay   = intval($m[3]);
+        $iMonth = intval($m[2]);
+        $iYear  = intval($m[1]);
+        $iH = intval($m[4]);
+        $iM = intval($m[5]);
+        $iS = isset($m[6]) ? intval($m[6]) : 0;
+        return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $iYear, $iMonth, $iDay, $iH, $iM, $iS); // 1985-10-28 00:59:35
+    case BX_DATA_DATE_TS:
+    case BX_DATA_DATE_TS_UTC:
+        $mixedData = trim($mixedData);
+        if (!preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $mixedData))
+            return false;
+        list($iYear, $iMonth, $iDay) = explode('-', $mixedData);
+        $iDay   = intval($iDay);
+        $iMonth = intval($iMonth);
+        $iYear  = intval($iYear);
+        $sFunc = BX_DATA_DATE_TS_UTC == $iDataType ? 'gmmktime' : 'mktime';
+        $iRet = $sFunc (0, 0, 0, $iMonth, $iDay, $iYear);
+        return $iRet > 0 ? $iRet : false;
+    case BX_DATA_DATETIME_TS:
+    case BX_DATA_DATETIME_TS_UTC:
+        if (!preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+):(\d+)#', $mixedData, $m) && !preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+)#', $mixedData, $m))
+            return bx_process_input ($mixedData, BX_DATA_DATE_TS, $mixedParams, $isCheckMagicQuotes);
+        $iDay   = $m[3];
+        $iMonth = $m[2];
+        $iYear  = $m[1];
+        $iH = $m[4];
+        $iM = $m[5];
+        $iS = isset($m[6]) ? $m[6] : 0;
+        $sFunc = BX_DATA_DATETIME_TS_UTC == $iDataType ? 'gmmktime' : 'mktime';
+        $iRet = $sFunc ($iH, $iM, $iS, $iMonth, $iDay, $iYear);
+        return $iRet > 0 ? $iRet : false;
+
+    case BX_DATA_HTML:
+        return clear_xss($mixedData);
+    case BX_DATA_TEXT_MULTILINE:
+        return nl2br(htmlspecialchars_adv($mixedData));
+    case BX_DATA_TEXT:
+    default:
+        return $mixedData;
+    }
+}
+
+/*
+ * Functions to process user output.
+ * Always use this function before output data which was entered by user before.
+ * @param $mixedData string to process
+ * @param $iDataType how to handle data, possible valies the same as in bx_process_input function, see bx_process_input.
+ * @param $mixedParams optional parameters to pass for validation
+ * @return the filtered data, or FALSE if the filter fails.
+ */
+function bx_process_output ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false)
+{
+    if (is_array($mixedData)) {
+        foreach ($mixedData as $k => $v)
+            $mixedData[$k] = bx_process_output($v, $iDataType, $mixedParams);
+        return $mixedData;
+    }
+
+    switch ($iDataType) {
+    case BX_DATA_INT:
+        return filter_var($mixedData, FILTER_VALIDATE_INT);
+    case BX_DATA_FLOAT:
+        return filter_var($mixedData, FILTER_VALIDATE_FLOAT);
+    case BX_DATA_CHECKBOX:
+        return 'on' == trim($mixedData) ? 'on' : '';
+
+    case BX_DATA_DATE:
+    case BX_DATA_DATETIME:
+        return $mixedData;
+    case BX_DATA_DATE_TS:
+        return empty($mixedData) ? '' : date("Y-m-d", (int)$mixedData);
+    case BX_DATA_DATE_TS_UTC:
+        return empty($mixedData) ? '' : gmdate("Y-m-d", (int)$mixedData);
+    case BX_DATA_DATETIME_TS:
+        return empty($mixedData) ? '' : date("Y-m-d H:i", (int)$mixedData);
+    case BX_DATA_DATETIME_TS_UTC:
+        return empty($mixedData) ? '' : gmdate("Y-m-d H:i", (int)$mixedData);
+
+    case BX_DATA_HTML:
+        return $mixedData;
+    case BX_DATA_TEXT_MULTILINE:
+        return $mixedData;
+    case BX_DATA_TEXT:
+    default:
+        return htmlspecialchars_adv($mixedData);
+    }
+}
+
+/*
+ * This function apply bx_process_input and then bx_process_output.
+ * Use this function to output data immediately after receiving, without saving to database.
+ * Patams are the same as bx_process_input function - @see bx_process_input
+ */
+function bx_process_pass ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false, $isCheckMagicQuotes = true)
+{
+    return bx_process_output(bx_process_input ($mixedData, $iDataType, $mixedParams, $isCheckMagicQuotes), $iDataType, $mixedParams);
+}
+
+/**
+ * DEPRECATED
+ * use bx_process_input + bx_process_output instead
+ * --------
+ * function for processing pass data
+ *
+ * This function cleans the GET/POST/COOKIE data if magic_quotes_gpc() is on
+ * for data which should be outputed immediately after submit
+ */
+/*
+function process_pass_data( $text, $strip_tags = 0 )
+{
+    if ( $strip_tags )
+        $text = strip_tags($text);
+
+    if ( !get_magic_quotes_gpc() )
+        return $text;
+    else
+        return stripslashes($text);
+}
+*/
+
+/*
+ * function for output data from database into html
+ */
+function htmlspecialchars_adv( $string )
+{
+    return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
+}
+
+/**
+ * Send mail to user by parsing email template
+ */
+function sendMailTemplate($sTemplateName, $iAccountId = 0, $iProfileId = 0, $aReplaceVars = array(), $iEmailType = BX_EMAIL_NOTIFY)
+{
+    $oProfile = BxDolProfile::getInstance($iProfileId);
+
+    $oAccount = $iAccountId ? BxDolAccount::getInstance($iAccountId) : ($oProfile ? $oProfile->getAccountObject() : null);
+
+    $oEmailTemplates = BxDolEmailTemplates::getInstance();
+
+    if (!$oAccount || !$oProfile || !$oEmailTemplates)
+        return false;
+
+    $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, $aReplaceVars, $iAccountId, (int)$iProfileId);
+    if (!$aTemplate)
+        return false;
+
+    return sendMail($oAccount->getEmail(), $aTemplate['Subject'], $aTemplate['Body'], 0, array(), $iEmailType);
+}
+
+/**
+ * Send system email 
+ */
+function sendMailTemplateSystem($sTemplateName, $aReplaceVars = array(), $iEmailType = BX_EMAIL_SYSTEM)
+{
+    $oEmailTemplates = BxDolEmailTemplates::getInstance();
+
+    if (!$oEmailTemplates)
+        return false;
+
+    $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, $aReplaceVars);
+    if (!$aTemplate)
+        return false;
+
+    return sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body'], 0, array(), $iEmailType);
+}
+/**
+ * Send email function
+ *
+ * @param $sRecipientEmail - Email where email should be send
+ * @param $sMailSubject - subject of the message
+ * @param $sMailBody - Body of the message
+ * @param $iRecipientID - ID of recipient profile
+ * @param $aPlus - Array of additional information
+ * @param $iEmailType - email message type: BX_EMAIL_SYSTEM, BX_EMAIL_NOTIFY or BX_EMAIL_MASS
+ * @param $sEmailFlag - use 'html' for HTML email message
+ * @param $isDisableAlert - disable alert
+ * @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)
+{
+    // make sure that recipient's email is valid and message isn't empty
+    if (!$sMailBody || !$sRecipientEmail || preg_match('/\(2\)$/', $sRecipientEmail))
+        return false;
+
+    // get recipient account
+    $oAccount = !$isDisableAlert ? BxDolAccount::getInstance($sRecipientEmail) : null;
+    $aAccountInfo = $oAccount ? $oAccount->getInfo() : false;
+
+    // don't send bulk emails if user didn't subscribed to site news or email is unconfirmed
+    if ($aAccountInfo && BX_EMAIL_MASS == $iEmailType && (!$aAccountInfo['email_confirmed'] || !$aAccountInfo['receive_news']))
+        return false;
+
+    // don't send email notifications if user didn't subscribed to notifications or email is unconfirmed
+    if ($aAccountInfo && BX_EMAIL_NOTIFY == $iEmailType && (!$aAccountInfo['email_confirmed'] || !$aAccountInfo['receive_updates']))
+        return false;
+
+    // if profile id is provided - get profile's info
+    $aRecipientInfo = false;
+    if ($iRecipientID) {
+        $oProfile = BxDolProfile::getInstance($iRecipientID);
+        if ($oProfile)
+            $aRecipientInfo = $oProfile->getInfo();
+    }
+
+    // get site vars
+    $sEmailNotify = !$isDisableAlert ? getParam('site_email_notify') : $sRecipientEmail;
+    $sSiteTitle = !$isDisableAlert ? getParam('site_title') : 'UNA ' . BX_DOL_VERSION;
+
+    // add unsubscribe link
+    if (!$isDisableAlert && empty($aPlus['unsubscribe'])) {
+        $aPlus['unsubscribe'] = '';
+        if ($oAccount && (BX_EMAIL_MASS == $iEmailType || BX_EMAIL_NOTIFY == $iEmailType))
+            $aPlus['unsubscribe'] = ($sLink = $oAccount->getUnsubscribeLink($iEmailType)) ? '<a href="' . BX_DOL_URL_ROOT . $sLink . '">' . _t('_sys_et_txt_unsubscribe') . '</a>' : '';
+    }
+
+    // parse template
+    if ($aPlus || $iRecipientID) {
+        if(!is_array($aPlus))
+            $aPlus = array();
+        $oEmailTemplates = BxDolEmailTemplates::getInstance();
+        $sMailSubject = $oEmailTemplates->parseContent($sMailSubject, $aPlus, $iRecipientID);
+        $sMailBody = $oEmailTemplates->parseContent($sMailBody, $aPlus, $iRecipientID);
+    }
+
+    // email message headers
+    $sMailHeader = "From: =?UTF-8?B?" . base64_encode( $sSiteTitle ) . "?= <{$sEmailNotify}>";
+    $sMailParameters = "-f{$sEmailNotify}";
+    $sMailSubject = '=?UTF-8?B?' . base64_encode( $sMailSubject ) . '?=';
+    $sMailHeader = "MIME-Version: 1.0\r\n" . $sMailHeader;
+
+    // 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,
+        'override_result' => &$bResult,
+    );
+
+    // system alert
+    if (!$isDisableAlert) {
+        bx_alert('system', 'before_send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+        if ($bResult !== null)
+            return $bResult;
+        unset($aAlert['override_result']);
+    }
+
+    // send mail
+    if( 'html' == $sEmailFlag) {
+        $sMailHeader = "Content-type: text/html; charset=UTF-8\r\n" . $sMailHeader;
+        $iSendingResult = mail( $sRecipientEmail, $sMailSubject, $sMailBody, $sMailHeader, $sMailParameters );
+    } else {
+        $sMailHeader = "Content-type: text/plain; charset=UTF-8\r\n" . $sMailHeader;
+        $sMailBody = html2txt($sMailBody);
+        $iSendingResult = mail( $sRecipientEmail, $sMailSubject, html2txt($sMailBody), $sMailHeader, $sMailParameters );
+    }
+
+    // system alert
+    if (!$isDisableAlert)
+        bx_alert('system', 'send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+
+    return $iSendingResult;
+}
+
+/*
+ * Getting an array with Templates' Names
+ */
+function get_templates_array($bEnabledOnly = true, $bShortInfo = true)
+{
+    $oDb = BxDolDb::getInstance();
+
+    $sWhereAddon = $bEnabledOnly ? " AND `enabled`='1'" : "";
+
+    if($bShortInfo)
+        return $oDb->getPairs("SELECT `uri`, `title` FROM `sys_modules` WHERE 1 AND `type`='" . BX_DOL_MODULE_TYPE_TEMPLATE . "'" . $sWhereAddon, "uri", "title");
+    else
+        return $oDb->getAllWithKey("SELECT * FROM `sys_modules` WHERE 1 AND `type`='" . BX_DOL_MODULE_TYPE_TEMPLATE . "'" . $sWhereAddon, "uri");
+}
+
+function extFileExists( $sFileSrc )
+{
+    return (file_exists( $sFileSrc ) && is_file( $sFileSrc )) ? true : false;
+}
+
+function getVisitorIP()
+{
+    $ip = "0.0.0.0";
+    if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+    } elseif (isset( $_SERVER['HTTP_CLIENT_IP']) && !empty($_SERVER['HTTP_CLIENT_IP'])) {
+        $ip = explode(".",$_SERVER['HTTP_CLIENT_IP']);
+        $ip = $ip[3].".".$ip[2].".".$ip[1].".".$ip[0];
+    } elseif (!isset( $_SERVER['HTTP_X_FORWARDED_FOR']) || empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        if (!isset( $_SERVER['HTTP_CLIENT_IP']) && empty($_SERVER['HTTP_CLIENT_IP']) && isset($_SERVER['REMOTE_ADDR']))
+            $ip = $_SERVER['REMOTE_ADDR'];
+    }
+    return $ip;
+}
+
+function genFlag($sLang = '', $oTemplate = null)
+{
+    if (!$oTemplate)
+        $oTemplate = BxDolTemplate::getInstance();
+    $oTemplate->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flag-icon-css/css/|flag-icon.min.css');
+    $sFlag = BxDolLanguages::getInstance()->getLangFlag($sLang);
+    return '<span title="' . $sFlag . '" class="flag-icon flag-icon-' . $sFlag . '"></span>';
+}
+
+// print debug information ( e.g. arrays )
+function echoDbg( $what, $desc = '' )
+{
+    if ( $desc )
+        echo "<b>$desc:</b> ";
+    echo "<pre>";
+        print_r( $what );
+    echo "</pre>\n";
+}
+
+function echoDbgLog($mWhat, $sDesc = '', $sFileName = 'debug.log')
+{
+    $sCont =
+        '--- ' . date('r') . ' (' . BX_DOL_START . ") ---\n" .
+        $sDesc . "\n" .
+        print_r($mWhat, true) . "\n\n\n";
+
+    $rFile = fopen(BX_DIRECTORY_PATH_LOGS . $sFileName, 'a');
+    fwrite($rFile, $sCont);
+    fclose($rFile);
+}
+
+function echoJson($a)
+{
+	header('Content-type: text/html; charset=utf-8');
+
+	echo json_encode($a);
+}
+
+function clear_xss($val)
+{
+    // HTML Purifier plugin
+    global $oHtmlPurifier;
+    if (!isset($oHtmlPurifier) && !$GLOBALS['logged']['admin']) {
+
+        require_once( BX_DIRECTORY_PATH_PLUGINS . 'htmlpurifier/HTMLPurifier.standalone.php' );
+
+        HTMLPurifier_Bootstrap::registerAutoload();
+
+        $oConfig = HTMLPurifier_Config::createDefault();
+
+        $oConfig->set('Cache.SerializerPath', rtrim(BX_DIRECTORY_PATH_CACHE, '/'));
+        $oConfig->set('Cache.SerializerPermissions', BX_DOL_DIR_RIGHTS);
+
+        $oConfig->set('HTML.SafeObject', 'true');
+        $oConfig->set('Output.FlashCompat', 'true');
+        $oConfig->set('HTML.FlashAllowFullScreen', 'true');
+
+        if (getParam('sys_add_nofollow')) {
+            $sHost = parse_url(BX_DOL_URL_ROOT, PHP_URL_HOST);
+            $oConfig->set('URI.Host', $sHost);
+            $oConfig->set('HTML.Nofollow', 'true');
+        }
+
+        $oConfig->set('Filter.Custom', array (new HTMLPurifier_Filter_YouTube(), new HTMLPurifier_Filter_YoutubeIframe(), new HTMLPurifier_Filter_AddBxLinksClass()));
+
+	    $oConfig->set('HTML.DefinitionID', 'html5-definitions');
+		$oConfig->set('HTML.DefinitionRev', 1);
+		if ($def = $oConfig->maybeGetRawHTMLDefinition()) {
+		    $def->addElement('section', 'Block', 'Flow', 'Common');
+		    $def->addElement('nav',     'Block', 'Flow', 'Common');
+		    $def->addElement('article', 'Block', 'Flow', 'Common');
+		    $def->addElement('aside',   'Block', 'Flow', 'Common');
+		    $def->addElement('header',  'Block', 'Flow', 'Common');
+		    $def->addElement('footer',  'Block', 'Flow', 'Common');
+		    $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
+		        'src' => 'URI',
+		        'type' => 'Text',
+		        'width' => 'Length',
+		        'height' => 'Length',
+		        'poster' => 'URI',
+		        'preload' => 'Enum#auto,metadata,none',
+		        'controls' => 'Bool',
+		    ));
+		    $def->addElement('source', 'Block', 'Flow', 'Common', array(
+		        'src' => 'URI',
+		        'type' => 'Text',
+		    ));
+		}
+
+        $oHtmlPurifier = new HTMLPurifier($oConfig);
+    }
+
+    if (!$GLOBALS['logged']['admin'])
+        $val = $oHtmlPurifier->purify($val);
+
+    bx_alert('system', 'clear_xss', 0, 0, array('oHtmlPurifier' => $oHtmlPurifier, 'return_data' => &$val));
+
+    return $val;
+}
+
+//--------------------------------------- friendly permalinks --------------------------------------//
+//------------------------------------------- main functions ---------------------------------------//
+function uriGenerate ($s, $sTable, $sField, $sEmpty = '-')
+{
+    $s = uriFilter($s, $sEmpty);
+    if(uriCheckUniq($s, $sTable, $sField))
+        return $s;
+
+    // cut off redundant part
+    if(get_mb_len($s) > 240)
+        $s = get_mb_substr($s, 0, 240);
+
+    // try to add date
+    $s .= '-' . date('Y-m-d');
+    if(uriCheckUniq($s, $sTable, $sField))
+        return $s;
+
+    // try to add number
+    for($i = 0 ; $i < 999 ; ++$i)
+        if(uriCheckUniq($s . '-' . $i, $sTable, $sField))
+            return ($s . '-' . $i);
+
+    return rand(0, 999999999);
+}
+
+function uriFilter ($s, $sEmpty = '-')
+{
+    if (BxTemplConfig::getInstance()->bAllowUnicodeInPreg)
+        $s = get_mb_replace ('/[^\pL^\pN]+/u', '-', $s); // unicode characters
+    else
+        $s = get_mb_replace ('/([^\d^\w]+)/u', '-', $s); // latin characters only
+
+    $s = get_mb_replace ('/([-^]+)/', '-', $s);
+    $s = get_mb_replace ('/([-]+)$/', '', $s); // remove trailing dash
+    if (!$s) $s = $sEmpty;
+    return $s;
+}
+
+function uriCheckUniq ($s, $sTable, $sField)
+{
+    $oDb = BxDolDb::getInstance();
+
+    $sSql = $oDb->prepare("SELECT 1 FROM `$sTable` WHERE `$sField`=? LIMIT 1", $s);
+    return !$oDb->query($sSql);
+}
+
+function get_mb_replace ($sPattern, $sReplace, $s)
+{
+    return preg_replace ($sPattern, $sReplace, $s);
+}
+
+function get_mb_len ($s)
+{
+    return (function_exists('mb_strlen')) ? mb_strlen($s) : strlen($s);
+}
+
+function get_mb_substr ($s, $iStart, $iLen)
+{
+    return (function_exists('mb_substr')) ? mb_substr ($s, $iStart, $iLen) : substr ($s, $iStart, $iLen);
+}
+
+function bx_mb_substr_replace($s, $sReplace, $iPosStart, $iLength)
+{
+    return mb_substr($s, 0, $iPosStart) . $sReplace . mb_substr($s, $iPosStart + $iLength);
+}
+
+function bx_mb_strpos ($s, $sReplacement, $iStart = 0)
+{
+    return mb_strpos($s, $sReplacement, $iStart);
+}
+
+/**
+ * Import class file, it automatically detects class path by its prefix or module array/name
+ *
+ * @param $sClassName - full class name or class postfix(withoit prefix) in the case of module class
+ * @param $mixedModule - module array or module name in the case of module class
+ */
+function bx_import($sClassName, $mixedModule = array())
+{
+    if (class_exists($sClassName, false))
+        return;
+
+    $aModule = false;
+    if ($mixedModule) {
+        if (is_array($mixedModule)) {
+            $aModule = $mixedModule;
+        } elseif (is_string($mixedModule)) {
+            $o = BxDolModule::getInstance($mixedModule);
+            $aModule = $o->_aModule;
+        } elseif (is_bool($mixedModule) && true === $mixedModule) {
+            $aModule = $GLOBALS['aModule'];
+        }
+    }
+
+    if ($aModule) {
+        if (class_exists($aModule['class_prefix'] . $sClassName, false))
+            return;
+        require_once (BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'classes/' . $aModule['class_prefix'] . $sClassName . '.php');
+        return;
+    }
+
+    if (0 === strncmp($sClassName, 'BxDol', 5)) {
+        if (0 === strncmp($sClassName, 'BxDolStudio', 11))
+            require_once(BX_DOL_DIR_STUDIO_CLASSES . $sClassName . '.php');
+        else
+            require_once(BX_DIRECTORY_PATH_CLASSES . $sClassName . '.php');
+        return;
+    }
+
+    if (0 === strncmp($sClassName, 'BxBase', 6)) {
+        if (0 === strncmp($sClassName, 'BxBaseMod', 9)) {
+            $aMatches = array();
+            if (preg_match('/BxBaseMod([A-Z][a-z]+)/', $sClassName, $aMatches)) {
+                require_once(BX_DIRECTORY_PATH_MODULES . 'base/' . strtolower($aMatches[1]) . '/classes/' . $sClassName . '.php');
+                return;
+            }
+        } if (0 === strncmp($sClassName, 'BxBaseStudio', 12)) {
+            require_once(BX_DOL_DIR_STUDIO_BASE . 'scripts/' . $sClassName . '.php');
+            return;
+        } else {
+            require_once(BX_DIRECTORY_PATH_BASE . 'scripts/' . $sClassName . '.php');
+            return;
+        }
+    }
+
+    if (0 === strncmp($sClassName, 'BxTempl', 7)) {
+        if(0 === strncmp($sClassName, 'BxTemplStudio', 13)) {
+            $sPath = BX_DIRECTORY_PATH_MODULES . BxDolStudioTemplate::getInstance()->getPath() . 'data/template/studio/scripts/' . $sClassName . '.php';
+        } else {
+            $sPath = BX_DIRECTORY_PATH_MODULES . BxDolTemplate::getInstance()->getPath() . 'data/template/system/scripts/' . $sClassName . '.php';
+        }
+
+        if (file_exists($sPath)) {
+            require_once($sPath);
+            return;
+        }
+
+        trigger_error ("bx_import fatal error: class (" . $sClassName . ") wasn't found", E_USER_ERROR);
+    }
+}
+
+/**
+ * used in spl_autoload_register() function, so no need to call bx_import for system classes
+ */
+function bx_autoload($sClassName)
+{
+    if (0 === strncmp($sClassName, 'BxDol', 5) || 0 === strncmp($sClassName, 'BxBase', 6) || 0 === strncmp($sClassName, 'BxTempl', 7))
+        bx_import($sClassName);
+}
+
+/**
+ * Gets an instance of class pathing necessary parameters if it's necessary.
+ *
+ * @param string $sClassName class name.
+ * @param array $aParams an array of parameters to be pathed to the constructor of the class.
+ * @param array $mixedModule an array with module description. Is used when the requested class is located in some module.
+ * @return unknown
+ */
+function bx_instance($sClassName, $aParams = array(), $mixedModule = array())
+{
+    if(isset($GLOBALS['bxDolClasses'][$sClassName]))
+        return $GLOBALS['bxDolClasses'][$sClassName];
+
+    if ($mixedModule) {
+        if (!is_array($mixedModule)) {
+            $o = BxDolModule::getInstance($mixedModule);
+            $mixedModule = $o->_aModule;
+        }
+        $sClassName = bx_ltrim_str($sClassName, $mixedModule['class_prefix']);
+        bx_import($sClassName, $mixedModule);
+        $sClassName = $mixedModule['class_prefix'] . $sClassName;
+    }
+
+    $oClass = new ReflectionClass($sClassName);
+
+    $GLOBALS['bxDolClasses'][$sClassName] = empty($aParams) ? $oClass->newInstance() : $oClass->newInstanceArgs($aParams);
+
+    return $GLOBALS['bxDolClasses'][$sClassName];
+}
+
+
+/**
+ * Escapes string/array ready to pass to js script with filtered symbols like ', " etc
+ *
+ * @param $mixedInput - string/array which should be filtered
+ * @param $iQuoteType - string escaping method: BX_ESCAPE_STR_AUTO(default), BX_ESCAPE_STR_APOS or BX_ESCAPE_STR_QUOTE
+ * @return converted string / array
+ */
+function bx_js_string ($mixedInput, $iQuoteType = BX_ESCAPE_STR_AUTO)
+{
+    $aUnits = array(
+        "\n" => "\\n",
+        "\r" => "",
+    );
+    if (BX_ESCAPE_STR_APOS == $iQuoteType) {
+        $aUnits["'"] = "\\'";
+        $aUnits['<script'] = "<scr' + 'ipt";
+        $aUnits['</script>'] = "</scr' + 'ipt>";
+    } elseif (BX_ESCAPE_STR_QUOTE == $iQuoteType) {
+        $aUnits['"'] = '\\"';
+        $aUnits['<script'] = '<scr" + "ipt';
+        $aUnits['</script>'] = '</scr" + "ipt>';
+    } else {
+        $aUnits['"'] = '&quot;';
+        $aUnits["'"] = '&apos;';
+        $aUnits["<script>"] = '&lt;script&gt;';
+        $aUnits["</script>"] = '&lt;/script&gt;';
+    }
+    return str_replace(array_keys($aUnits), array_values($aUnits), $mixedInput);
+}
+
+/**
+ * Return input string/array ready to pass to html attribute with filtered symbols like ', " etc
+ *
+ * @param $mixedInput - string/array which should be filtered
+ * @param $iQuoteType - string escaping BX_ESCAPE_STR_AUTO, BX_ESCAPE_STR_APOS or BX_ESCAPE_STR_QUOTE
+ * @return converted string / array
+ */
+function bx_html_attribute ($mixedInput, $iQuoteType = BX_ESCAPE_STR_AUTO)
+{
+    $aUnits = array ();
+    if (BX_ESCAPE_STR_APOS == $iQuoteType)
+        $aUnits["'"] = "\\'";
+    elseif (BX_ESCAPE_STR_QUOTE == $iQuoteType)
+        $aUnits['"'] = '&quot;';
+    else
+        $aUnits = array("\"" => "&quot;", "'" => "&apos;");
+
+    return str_replace(array_keys($aUnits), array_values($aUnits), $mixedInput);
+}
+
+/**
+ * Escapes string/array ready to pass to php script with filtered symbols like ', " etc
+ *
+ * @param mixed $mixedInput - string/array which should be filtered
+ * @return converted string / array
+ */
+function bx_php_string_apos ($mixedInput)
+{
+    return str_replace("'", "\\'", $mixedInput);
+}
+function bx_php_string_quot ($mixedInput)
+{
+    return str_replace('"', '\\"', $mixedInput);
+}
+
+/**
+ * Gets file contents by URL.
+ *
+ * @param string $sFileUrl - file URL to be read.
+ * @param array $aParams - an array of parameters to be pathed with URL.
+ * @param string $sMethod - post or get.
+ * @param array $aHeaders - custom headers.
+ * @param string $sHttpCode - HTTP code to return
+ * @param array $aBasicAuth - array with 'user' and 'password' for Basic HTTP Auth
+ * @return string the file's contents.
+ */
+function bx_file_get_contents($sFileUrl, $aParams = array(), $sMethod = 'get', $aHeaders = array(), &$sHttpCode = null, $aBasicAuth = array())
+{
+    $bChangeTimeout = false;
+
+    if ('post' != $sMethod)
+    	$sFileUrl = bx_append_url_params($sFileUrl, $aParams);
+
+    $sResult = '';
+    if(function_exists('curl_init')) {
+        $rConnect = curl_init();
+
+        curl_setopt($rConnect, CURLOPT_USERAGENT, 'UNA ' . BX_DOL_VERSION);
+        curl_setopt($rConnect, CURLOPT_TIMEOUT, 10);
+        curl_setopt($rConnect, CURLOPT_URL, $sFileUrl);
+        curl_setopt($rConnect, CURLOPT_HEADER, NULL === $sHttpCode ? false : true);
+        curl_setopt($rConnect, CURLOPT_RETURNTRANSFER, 1);
+
+        if ($bChangeTimeout) {
+            curl_setopt($rConnect, CURLOPT_CONNECTTIMEOUT, 3);
+            curl_setopt($rConnect, CURLOPT_TIMEOUT, 3);
+        }
+
+        if (!ini_get('open_basedir'))
+            curl_setopt($rConnect, CURLOPT_FOLLOWLOCATION, 1);
+
+        if ($aHeaders)
+            curl_setopt($rConnect, CURLOPT_HTTPHEADER, $aHeaders);
+
+        if ($aBasicAuth)
+            curl_setopt($rConnect, CURLOPT_USERPWD, $aBasicAuth['user'] . ':' . $aBasicAuth['password']);
+
+        if ('post' == $sMethod) {
+            curl_setopt($rConnect, CURLOPT_POST, true);
+            curl_setopt($rConnect, CURLOPT_POSTFIELDS, http_build_query($aParams));
+        }
+
+        $sAllCookies = '';
+        foreach($_COOKIE as $sKey=>$sValue){
+            $sAllCookies .= $sKey . '=' . $sValue . ';';
+        }
+        curl_setopt($rConnect, CURLOPT_COOKIE, $sAllCookies);
+
+        $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 ($bChangeTimeout) {
+            $iSaveTimeout = ini_get('default_socket_timeout');
+            ini_set('default_socket_timeout', 3);
+        }
+
+        $sResult = @file_get_contents($sFileUrl);
+
+        if ($bChangeTimeout && false !== $iSaveTimeout) {
+            ini_set('default_socket_timeout', $iSaveTimeout);
+        }
+    }
+
+    return $sResult;
+}
+
+function bx_get_site_info($sSourceUrl, $aProcessAdditionalTags = array())
+{
+    $aResult = array();
+    $sContent = bx_file_get_contents($sSourceUrl);
+
+    if ($sContent) {
+        $sCharset = '';
+        preg_match("/<meta.+charset=([A-Za-z0-9-]+).+>/i", $sContent, $aMatch);
+        if (isset($aMatch[1]))
+            $sCharset = $aMatch[1];
+
+        if (preg_match("/<title[^>]*>(.*)<\/title>/i", $sContent, $aMatch))
+            $aResult['title'] = $aMatch[1];
+        else
+            $aResult['title'] = parse_url($sSourceUrl, PHP_URL_HOST);
+
+        $aResult['description'] = bx_parse_html_tag($sContent, 'meta', 'name', 'description', 'content', $sCharset);
+        $aResult['keywords'] = bx_parse_html_tag($sContent, 'meta', 'name', 'keywords', 'content', $sCharset);
+
+        if ($aProcessAdditionalTags) {
+
+            foreach ($aProcessAdditionalTags as $k => $a) {
+                $aResult[$k] = bx_parse_html_tag(
+                    $sContent, 
+                    isset($a['tag']) ? $a['tag'] : 'meta', 
+                    isset($a['name_attr']) ? $a['name_attr'] : 'itemprop', 
+                    isset($a['name']) ? $a['name'] : $k, 
+                    isset($a['content_attr']) ? $a['content_attr'] : 'content', 
+                    $sCharset); 
+            }
+
+        }
+    }
+
+    return $aResult;
+}
+
+function bx_parse_html_tag ($sContent, $sTag, $sAttrNameName, $sAttrNameValue, $sAttrContentName, $sCharset = false)
+{
+    if (!preg_match("/<{$sTag}\s+{$sAttrNameName}[='\" ]+{$sAttrNameValue}['\"]\s+{$sAttrContentName}[='\" ]+([^'>\"]*)['\"][^>]*>/i", $sContent, $aMatch) || !isset($aMatch[1]))
+        preg_match("/<{$sTag}\s+{$sAttrContentName}[='\" ]+([^'>\"]*)['\"]\s+{$sAttrNameName}[='\" ]+{$sAttrNameValue}['\"][^>]*>/i", $sContent, $aMatch);
+
+    $s = isset($aMatch[1]) ? $aMatch[1] : '';
+
+    if ($s && $sCharset)
+        $s = mb_convert_encoding($s, 'UTF-8', $sCharset);
+
+    return $s;
+}
+
+// calculation ini_get('upload_max_filesize') in bytes as example
+function return_bytes($val)
+{
+    $val = trim($val);
+    $last = strtolower($val{strlen($val)-1});
+    $val = (int)$val;
+    switch($last) {
+        // The 'G' modifier is available since PHP 5.1.0
+        case 'k':
+            $val *= 1024;
+            break;
+        case 'm':
+            $val *= 1024 * 1024;
+            break;
+        case 'g':
+            $val *= 1024 * 1024 * 1024;
+            break;
+    }
+    return $val;
+}
+
+// Generate Random Password
+function genRndPwd($iLength = 8, $bSpecialCharacters = true)
+{
+    $sPassword = '';
+    $sChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+
+    if($bSpecialCharacters === true)
+        $sChars .= "!?=/&+,.";
+
+    srand((double)microtime()*1000000);
+    for($i = 0; $i < $iLength; $i++) {
+        $x = mt_rand(0, strlen($sChars) -1);
+        $sPassword .= $sChars{$x};
+    }
+
+    return $sPassword;
+}
+
+// Generate Random Salt for Password encryption
+function genRndSalt()
+{
+    return genRndPwd(8, true);
+}
+
+/**
+ * Encrypt user password.
+ * When BX_PWD_ALGO is 'custom', then bx_custom_pwd_encrypt function with custom algorythm must be defined in inc/header.inc.php file.
+ * @param $sPwd clear password
+ * @param $sSalt salt
+ * @return password hash
+ */ 
+function encryptUserPwd($sPwd, $sSalt)
+{
+	$sAlgo = defined('BX_PWD_ALGO') ? BX_PWD_ALGO : '';
+
+    switch ($sAlgo) {
+        
+        case 'custom':
+            return bx_custom_pwd_encrypt($sPwd, $sSalt); 
+
+    	case 'crypt':
+            return crypt($sPwd, BX_PWD_ALGO_SALT);
+
+        case 'sha1_crypt_salt':
+            return sha1(crypt($sPwd, BX_PWD_ALGO_SALT) . $sSalt);
+
+        case 'sha1_md5_salt':
+        default:
+            return sha1(md5($sPwd) . $sSalt);
+    }
+}
+
+function bx_get ($sName, $sMethod = false)
+{
+    if (isset($_GET[$sName]) && ('get' == $sMethod || !$sMethod))
+        return $_GET[$sName];
+    elseif (isset($_POST[$sName]) && ('post' == $sMethod || !$sMethod))
+        return $_POST[$sName];
+    else
+        return false;
+}
+
+function bx_encode_url_params ($a, $aExcludeKeys = array (), $aOnlyKeys = false)
+{
+    $s = '';
+    foreach ($a as $sKey => $sVal) {
+        if (in_array($sKey, $aExcludeKeys))
+            continue;
+        if (false !== $aOnlyKeys && !in_array($sKey, $aOnlyKeys))
+            continue;
+        if (is_array($sVal)) {
+            foreach ($sVal as $sSubVal) {
+                $s .= rawurlencode($sKey) . '[]=' . rawurlencode(is_array($sSubVal) ? 'array' : $sSubVal) . '&';
+            }
+        } else {
+            $s .= rawurlencode($sKey) . '=' . rawurlencode($sVal) . '&';
+        }
+    }
+    return $s;
+}
+
+function bx_append_url_params ($sUrl, $mixedParams)
+{
+    if (!$mixedParams)
+        return $sUrl;
+
+    $sParams = false == strpos($sUrl, '?') ? '?' : '&';
+
+    if (is_array($mixedParams)) {
+        foreach($mixedParams as $sKey => $sValue) {
+            if (!is_array($sValue)) {
+                $sParams .= $sKey . '=' . $sValue . '&';
+            }
+            else {
+                foreach($sValue as $sSubValue)
+                    $sParams .= $sKey . '[]=' . $sSubValue . '&';
+            }
+        }
+        $sParams = substr($sParams, 0, -1);
+    } else {
+        $sParams .= $mixedParams;
+    }
+    return $sUrl . $sParams;
+}
+
+function bx_rrmdir($directory)
+{
+    if (substr($directory,-1) == "/")
+        $directory = substr($directory,0,-1);
+
+    if (!file_exists($directory) || !is_dir($directory))
+        return false;
+    elseif (!is_readable($directory))
+        return false;
+
+    if (!($directoryHandle = opendir($directory)))
+        return false;
+
+    while ($contents = readdir($directoryHandle)) {
+        if ($contents != '.' && $contents != '..') {
+            $path = $directory . "/" . $contents;
+
+            if (is_dir($path))
+                bx_rrmdir($path);
+            else
+                unlink($path);
+        }
+    }
+
+    closedir($directoryHandle);
+
+    if (!rmdir($directory))
+        return false;
+
+    return true;
+}
+
+function bx_clear_folder ($sPath, $aExts = array ())
+{
+    if (substr($$sPath,-1) == "/")
+        $sPath = substr($sPath,0,-1);
+
+    if (!file_exists($sPath) || !is_dir($sPath))
+        return false;
+    elseif (!is_readable($sPath))
+        return false;
+
+    if (!($h = opendir($sPath)))
+        return false;
+
+    while ($sFile = readdir($h)) {
+        if ('.' == $sFile || '..' == $sFile)
+            continue;
+
+        $sFullPath = $sPath . '/' . $sFile;
+
+        if (is_dir($sFullPath))
+            continue;
+
+        if (!$aExts || (($sExt = pathinfo($sFullPath, PATHINFO_EXTENSION)) && in_array($sExt, $aExts)))
+            @unlink($sFullPath);
+    }
+
+    closedir($h);
+
+    return true;
+}
+
+function bx_ltrim_str ($sString, $sPrefix, $sReplace = '')
+{
+    if ($sReplace && substr($sString, 0, strlen($sReplace)) == $sReplace)
+        return $sString;
+    if (substr($sString, 0, strlen($sPrefix)) == $sPrefix)
+        return $sReplace . substr($sString, strlen($sPrefix));
+    return $sString;
+}
+
+function bx_rtrim_str ($sString, $sPrefix, $sReplace = '')
+{
+    if ($sReplace && substr($sString, -strlen($sReplace)) == $sReplace)
+        return $sString;
+    if (substr($sString, -strlen($sPrefix)) == $sPrefix)
+        return substr($sString, 0, -strlen($sPrefix)) . $sReplace;
+    return $sString;
+}
+
+/**
+ * Convert array to attributes string
+ *
+ * Example:
+ * @code
+ * $a = array('name' => 'test', 'value' => 5);
+ * $s = bx_convert_array2attrs($a);
+ * echo $s; // outputs: name="test" value="5"
+ * @endcode
+ *
+ * @param $a - array of attributes
+ * @param $sClasses - classes to merge with 'class' attribute
+ * @param $sStyles - styles to merge with 'style' attribute
+ * @return string
+ */
+function bx_convert_array2attrs ($a, $sClasses = false, $sStyles = false)
+{
+    $sRet = '';
+
+    if (!$a || !is_array($a))
+        $a = array();
+
+    if ($sClasses) {
+        $sClasses = trim($sClasses);
+        $a['class'] = $sClasses . (!empty($a['class']) ? ' ' . $a['class'] : '');
+    }
+
+    if ($sStyles) {
+        $sStyles = trim($sStyles);
+        if (';' != $sStyles[strlen($sStyles)-1])
+            $sStyles .= ';';
+        $a['style'] = $sStyles . (!empty($a['style']) ? ' ' . $a['style'] : '');
+    }
+
+    foreach ($a as $sKey => $sValue) {
+        if(is_null($sValue)) // pass NULL values
+            continue;
+
+        $sValueC = bx_html_attribute($sValue, BX_ESCAPE_STR_QUOTE);
+
+        $sRet .= " $sKey=\"$sValueC\"";
+    }
+
+    return $sRet;
+}
+
+function bx_unicode_urldecode($s)
+{
+    preg_match_all('/%u([[:alnum:]]{4})/', $s, $a);
+
+    foreach ($a[1] as $uniord) {
+        $dec = hexdec($uniord);
+        $utf = '';
+
+        if ($dec < 128) {
+            $utf = chr($dec);
+        } else if ($dec < 2048) {
+            $utf = chr(192 + (($dec - ($dec % 64)) / 64));
+            $utf .= chr(128 + ($dec % 64));
+        } else {
+            $utf = chr(224 + (($dec - ($dec % 4096)) / 4096));
+            $utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64));
+            $utf .= chr(128 + ($dec % 64));
+        }
+
+        $s = str_replace('%u'.$uniord, $utf, $s);
+    }
+
+    return urldecode($s);
+}
+
+/**
+ * Raise an alert
+ * @param string $sUnit - system type
+ * @param string $sAction - system action
+ * @param int $iObjectId - object id
+ * @param int $iSender - sender (action's author) profile id, if it is false - then currectly logged in profile id is used
+ * @param array $aExtras - extra parameters 
+ */
+function bx_alert($sUnit, $sAction, $iObjectId, $iSender = false, $aExtras = array())
+{
+    $o = new BxDolAlerts($sUnit, $sAction, $iObjectId, $iSender, $aExtras);
+    $o->alert();
+}
+
+function bx_replace_markers($mixed, $aMarkers)
+{
+    if (empty($aMarkers))
+        return $mixed;
+
+    if (is_array($mixed)) {
+        foreach ($mixed as $sKey => $sValue)
+            $mixed[$sKey] = bx_replace_markers ($sValue, $aMarkers);
+    } else {
+        foreach ($aMarkers as $sKey => $sValue)
+            $mixed = str_replace('{' . $sKey . '}', $sValue, $mixed);
+    }
+
+    return $mixed;
+}
+
+function bx_site_hash($sSalt = '', $isSkipVersion = false)
+{
+    return md5($sSalt . ($isSkipVersion ? '' : bx_get_ver()) . BX_DOL_SECRET . BX_DOL_URL_ROOT);
+}
+
+/**
+ * Transform string to method name string, for example it changes 'some_method' string to 'SomeMethod' string
+ * @param $s string where words are separated with underscore
+ * @param $aWordsDelimiter word delimeters
+ * @return string where every word begins with capital letter
+ */
+function bx_gen_method_name ($s, $aWordsDelimiter = array('_'))
+{
+    return str_replace(' ', '', ucwords(str_replace($aWordsDelimiter, ' ', $s)));
+}
+
+/**
+ * Trigger user error
+ * @param $sMsg message to display
+ * @param $iNumLevelsBack add additional debug backtracing N levels back
+ */
+function bx_trigger_error ($sMsg, $iNumLevelsBack = 0)
+{
+    $a = debug_backtrace();
+    $sMsgAdd = "<br />\n related code in <b>{$a[$iNumLevelsBack]['file']}</b> on line <b>{$a[$iNumLevelsBack]['line']}</b> <br />\n";
+    trigger_error ($sMsg . $sMsgAdd, E_USER_ERROR);
+}
+
+/**
+ * Get system DB version, for files version @see BX_DOL_VERSION, these versions must match
+ */
+function bx_get_ver ($bInvalidateCache = false)
+{
+    $oDb = BxDolDb::getInstance();
+
+    if ($bInvalidateCache)
+        $oDb->cleanMemory('sys_version');
+    
+    $sQuery = $oDb->prepare("SELECT `version` FROM `sys_modules` WHERE `name` = 'system'");
+    return $oDb->fromMemory('sys_version', 'getOne', $sQuery);
+}
+
+/**
+ * Check if site maintetance mode is enabled.
+ * Maintetance mode is enabled when '.bx_maintenance' file exists in the script root folder, 
+ * please note that this is hidden file and some file managers don't show it.
+ * @param $bShowHttpError show 503 HTTP error if site is in mainenance mode
+ * @return true if site is in maintenance mode, or false otherwise
+ */
+function bx_check_maintenance_mode ($bShowHttpError = false)
+{
+    $bMaintetance = file_exists(BX_DIRECTORY_PATH_ROOT . BX_MAINTENANCE_FILE) && !defined('BX_DOL_UPGRADING');
+
+    if ($bMaintetance && $bShowHttpError)
+        bx_show_service_unavailable_error_and_exit ('Site is temporarily unavailable due to scheduled maintenance, please try again in a minute.', 600);
+
+    return $bMaintetance;
+}
+
+/**
+ * Check for minimal requirements.
+ * if BX_DISABLE_REQUIREMENTS_CHECK is defined then this requirements checking is skipped.
+ * @param $bShowHttpError show 503 HTTP error if site doesn't meet minimal requirements
+ * @return false if requirements are met, or array of errors of requirements aren't met
+ */
+function bx_check_minimal_requirements ($bShowHttpError = false)
+{
+    if (defined('BX_DISABLE_REQUIREMENTS_CHECK'))
+        return false;
+
+    $aErrors = array();
+
+    $aErrors[] = (ini_get('register_globals') == 0) ? '' : '<b>register_globals</b> is on (you need to disable it, or your site will be unsafe)';
+    $aErrors[] = (ini_get('safe_mode') == 0) ? '' : '<b>safe_mode</b> is on (you need to disable it)';
+    $aErrors[] = (version_compare(PHP_VERSION, '5.3.0', '<')) ? 'PHP version is too old (please update to <b>PHP 5.3.0</b> at least)' : '';
+    $aErrors[] = (!extension_loaded( 'mbstring')) ? '<b>mbstring</b> extension not installed (the script cannot work without it)' : '';
+    $aErrors[] = (ini_get('allow_url_include') == 0) ? '' : '<b>allow_url_include</b> is on (you need to disable it, or your site will be unsafe)';
+
+    $aErrors = array_diff($aErrors, array('')); // delete empty
+
+    $bFailedMinimalRequirements = !empty($aErrors);
+
+    if ($bFailedMinimalRequirements && $bShowHttpError) {
+        $sErrors = implode(" <br /> ", $aErrors);
+        bx_show_service_unavailable_error_and_exit($sErrors);
+    }
+
+    return $bFailedMinimalRequirements ? $aErrors : false;
+}
+
+/**
+ * Check if redirect to the correct hostname is required, for example redirect from site.com to www.site.com
+ * @param $bProcessRedirect process redirect and exit if needed
+ */
+function bx_check_redirect_to_correct_hostname ($bProcessRedirect = false)
+{
+    $aUrl = parse_url(BX_DOL_URL_ROOT);
+    $iPortDefault = 'https' == $aUrl['scheme'] ? '443' : '80';
+
+    $bRedirectRequired = isset($_SERVER['HTTP_HOST']) && 0 !== strcasecmp($_SERVER['HTTP_HOST'], $aUrl['host']) && 0 !== strcasecmp($_SERVER['HTTP_HOST'], $aUrl['host'] . ':' . (!empty($aUrl['port']) ? $aUrl['port'] : $iPortDefault));
+
+    if ($bRedirectRequired && $bProcessRedirect) {
+        $sPort = empty($aUrl['port']) || 80 == $aUrl['port'] || 443 == $aUrl['port'] ? '' : ':' . $aUrl['port'];
+        header("Location:{$aUrl['scheme']}://{$aUrl['host']}{$sPort}{$_SERVER['REQUEST_URI']}", true, 301);
+        exit;
+    }
+    
+    return $bRedirectRequired;
+}
+
+/**
+ * Check if redirect to remove install folder.
+ * If BX_SKIP_INSTALL_CHECK is defined then this redirect checking is skipped.
+ * @param $bProcessRedirect process redirect and exit if needed
+ */
+function bx_check_redirect_to_remove_install_folder ($bProcessRedirect = false)
+{
+    $bRemoveInstallFolder = !defined ('BX_SKIP_INSTALL_CHECK') && file_exists(BX_DIRECTORY_PATH_ROOT . 'install');
+
+    if ($bRemoveInstallFolder && $bProcessRedirect) {
+        header('Location:' . BX_DOL_URL_ROOT . 'install/index.php?action=remove_install');
+        exit;
+    }
+
+    return $bRemoveInstallFolder;
+}
+
+/**
+ * Show HTTP 503 service unavailable error and exit
+ */
+function bx_show_service_unavailable_error_and_exit ($sMsg = false, $iRetryAfter = 86400)
+{
+    header('HTTP/1.0 503 Service Unavailable', true, 503);
+    header('Retry-After: 600');
+    echo $sMsg ? $sMsg : 'Service temporarily unavailable';
+    exit;
+}
+
+/**
+ * The function is sumilar to php readfile, but it send all required headers and can send file by chunks and suports file seek
+ * @param $sPath path to file to output to the browser
+ * @param $sFilename filename without path, ig file is saved from browser, then this name is used, not used(empty) by default
+ * @param $sMimeType file mime type, by default 'application/octet-stream'
+ * @param $iCacheAge file cache age, by default 0
+ * @param $sCachePrivacy cache privacy 'public' (default value) or 'private'
+ * @return true on success or false on error
+ */
+function bx_smart_readfile($sPath, $sFilename = '', $sMimeType = 'application/octet-stream', $iCacheAge = 0, $sCachePrivacy = 'public')
+{
+    if (!file_exists($sPath))
+        return  false;
+
+    $fp = @fopen($sPath, 'rb');
+
+    $size   = filesize($sPath);
+    $length = $size;
+    $start  = 0;
+    $end    = $size - 1;
+
+    header('Content-type: ' . $sMimeType);
+    header('Cache-Control: ' . $sCachePrivacy . ', must-revalidate, max-age=' . $iCacheAge);
+    header("Accept-Ranges: 0-$length");
+    if ($sFilename)
+        header('Content-Disposition: inline; filename=' . $sFilename);
+
+    if (isset($_SERVER['HTTP_RANGE'])) {
+
+        $c_start = $start;
+        $c_end   = $end;
+
+        list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
+        if (strpos($range, ',') !== false) {
+            header('HTTP/1.1 416 Requested Range Not Satisfiable');
+            header("Content-Range: bytes $start-$end/$size");
+            return false;
+        }
+        if ($range == '-') {
+            $c_start = $size - substr($range, 1);
+        }else{
+            $range  = explode('-', $range);
+            $c_start = $range[0];
+            $c_end   = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
+        }
+        $c_end = ($c_end > $end) ? $end : $c_end;
+        if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
+            header('HTTP/1.1 416 Requested Range Not Satisfiable');
+            header("Content-Range: bytes $start-$end/$size");
+            return false;
+        }
+        $start  = $c_start;
+        $end    = $c_end;
+        $length = $end - $start + 1;
+        fseek($fp, $start);
+        header('HTTP/1.1 206 Partial Content');
+    }
+    header("Content-Range: bytes $start-$end/$size");
+    header("Content-Length: ".$length);
+
+
+    $buffer = 1024 * 8;
+    while(!feof($fp) && ($p = ftell($fp)) <= $end) {
+
+        if ($p + $buffer > $end) {
+            $buffer = $end - $p + 1;
+        }
+        set_time_limit(0);
+        echo fread($fp, $buffer);
+        flush();
+    }
+
+    fclose($fp);
+
+    return true;
+}
+
+/**
+ * Wrap in A tag links in TEXT string
+ * @param $text - text string without tags
+ * @param $sAttrs - attributes string to add to the added A tag
+ * @param $bHtmlSpecialChars - apply htmlspecialchars before processing
+ * @return string where all links are wrapped in A tag
+ */
+function bx_linkify($text, $sAttrs = '', $bHtmlSpecialChars = false)
+{
+    if ($bHtmlSpecialChars)
+        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+    $re = "@\b((https?://)|(www\.))(([0-9a-zA-Z_!~*'().&=+$%-]+:)?[0-9a-zA-Z_!~*'().&=+$%-]+\@)?(([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+\.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z]\.[a-zA-Z]{2,6})(:[0-9]{1,4})?((/[0-9a-zA-Z_!~*'().;?:\@&=+$,%#-]+)*/?)@";
+    preg_match_all($re, $text, $matches, PREG_OFFSET_CAPTURE);
+
+    $matches = $matches[0];
+
+    if ($i = count($matches))
+        $bAddNofollow = getParam('sys_add_nofollow') == 'on';
+
+    while ($i--)
+    {
+        $url = $matches[$i][0];
+        if (!preg_match('@^https?://@', $url))
+            $url = 'http://'.$url;
+
+        if (strncmp(BX_DOL_URL_ROOT, $url, strlen(BX_DOL_URL_ROOT)) !== 0) {
+            $sAttrs .= ' target="_blank" ';
+            if ($bAddNofollow)
+                $sAttrs .= ' rel="nofollow" ';
+        }
+
+        $text = substr_replace($text, '<a ' . $sAttrs . ' href="'.$url.'">'.$matches[$i][0].'</a>', $matches[$i][1], strlen($matches[$i][0]));
+    }
+
+    return $text;
+}
+
+/**
+ * Wrap in A tag links in HTML string, which aren't wrapped in A tag yet
+ * @param $sHtmlOrig - HTML string
+ * @param $sAttrs - attributes string to add to the added A tag
+ * @return modified HTML string, in case of errror original string is returned
+ */
+function bx_linkify_html($sHtmlOrig, $sAttrs = '') 
+{
+    if (!trim($sHtmlOrig))
+        return $sHtmlOrig;
+
+    $sId = 'bx-linkify-' . md5(microtime());
+    $dom = new DOMDocument();
+    @$dom->loadHTML('<?xml encoding="UTF-8"><div id="' . $sId . '">' . $sHtmlOrig . '</div>');
+    $xpath = new DOMXpath($dom);
+
+    foreach ($xpath->query('//text()') as $text) {
+        $frag = $dom->createDocumentFragment();
+        $frag->appendXML(bx_linkify($text->nodeValue, $sAttrs, true));
+        $text->parentNode->replaceChild($frag, $text);
+    }
+
+    if (version_compare(PHP_VERSION, '5.3.6') >= 0)
+        $s = $dom->saveHTML($dom->getElementById($sId));
+    else
+        $s = $dom->saveXML($dom->getElementById($sId), LIBXML_NOEMPTYTAG);
+
+    if (false === $s) // in case of error return original string
+        return $sHtmlOrig;
+
+    if (false !== ($iPos = mb_strpos($s, '<html><body>')) && $iPos < mb_strpos($s, $sId))
+        $s = mb_substr($s, $iPos + 12, -15); // strip <html><body> tags and everything before them
+
+    return mb_substr($s, 54, -6); // strip added tags
+}
+
+/** @} */

+ 12 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/inc/version.inc.php

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

+ 290 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/connect/classes/BxBaseModConnectModule.php

@@ -0,0 +1,290 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseConnect Base classes for OAuth connect modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModConnectModule extends BxBaseModGeneralModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    public function serviceGetProfilesModules ()
+    {
+        $aModules = array();
+        $a = BxDolService::call('system', 'get_profiles_modules', array(), 'TemplServiceProfiles');
+        foreach ($a as &$aModule)
+            $aModules[$aModule['name']] = $aModule['title'];
+        return $aModules;
+    }
+
+    public function serviceGetPrivacyGroups ()
+    {
+        $oPrivacyQuery = new BxDolPrivacyQuery();
+        $a = $oPrivacyQuery->getGroupsBy(array('type' => 'active'));
+        $aGroups = array();
+        foreach ($a as $r) {
+            if (!(int)$r['active'])
+               continue;
+            $aGroups[$r['id']] = _t($r['title']);
+        }
+        return $aGroups;
+    }
+    
+    /**
+     * Logged profile
+     *
+     * @param $iProfileId integer
+     * @param $sPassword string
+     * @param $sCallbackUrl
+     * @param $bRedirect boolean
+     * @return void
+     */
+    function setLogged($iProfileId, $sCallbackUrl = '', $bRedirect = true, $bRememberMe = false)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+ 
+        bx_login($oProfile->getAccountId(), $bRememberMe);
+
+        if ($bRedirect) {
+            $sCallbackUrl = $sCallbackUrl
+                ? $sCallbackUrl
+                : $this -> _oConfig -> sDefaultRedirectUrl;
+
+            header('Location: ' . $sCallbackUrl);
+        }
+    }
+
+    /**
+     * Create new profile;
+     *
+     * @param  : $aProfileInfo (array) - remote profile's information;
+     *
+     * @param  : $sAlternativeName (string) - profiles alternative nickname;
+     */
+    function _createProfile($aProfileInfo, $sAlternativeName = '')
+    {
+        $mixed = $this->_createProfileRaw($aProfileInfo, $sAlternativeName);
+
+        // display error
+        if (is_string($mixed)) {
+            $this->_oTemplate->getPage(_t($this->_oConfig->sDefaultTitleLangKey), DesignBoxContent(_t($this->_oConfig->sDefaultTitleLangKey), MsgBox($mixed)));
+            exit;
+        } 
+
+        // display join page
+        if (is_array($mixed) && isset($mixed['join_page_redirect'])) {
+            $this->_getJoinPage($mixed['profile_fields'], $mixed['remote_profile_info']['id']);
+            exit;
+        } 
+
+        // continue profile creation
+        if (is_array($mixed) && isset($mixed['profile_id'])) {
+            $iProfileId = (int)$mixed['profile_id'];
+
+            //redirect to other page
+            header('location:' . $this->_getRedirectUrl($iProfileId, $mixed['existing_profile']));
+            exit;
+        }
+
+        $this->_oTemplate->getPage( _t($this->_oConfig->sDefaultTitleLangKey), MsgBox(_t('_Error Occured')) );
+        exit;
+    }
+
+    /**
+     * @param $aProfileInfo - remote profile info
+     * @param $sAlternativeName - suffix to add to NickName to make it unique
+     * @return profile array info, ready for the local database
+     */
+    protected function _convertRemoteFields($aProfileInfo, $sAlternativeName = '')
+    {
+    }
+
+    /**
+     * Create new profile;
+     *
+     * @param  : $aProfileInfo (array) - remote profile's information;
+     *
+     * @param  : $sAlternativeName (string) - profiles alternative nickname;
+     * @return : error string or error or request invite form or profile info array on success
+     */
+    function _createProfileRaw($aProfileInfo, $sAlternativeName = '', $isAutoFriends = true, $isSetLoggedIn = true)
+    {
+        // join by invite only
+        if (BxDolRequest::serviceExists('bx_invites', 'account_add_form_check') && $sCode = BxDolService::call('bx_invites', 'account_add_form_check'))
+            return $sCode;
+
+        // convert fields to unique format
+        $aFieldsProfile = $aFieldsAccount = $this->_convertRemoteFields($aProfileInfo, $sAlternativeName);
+        if (empty($aFieldsProfile['email']))
+            return _t('_Incorrect Email');
+
+        // prepare fields for particular module
+        $aFieldsAccount = BxDolService::call('system', 'prepare_fields', array($aFieldsAccount));
+        $aFieldsProfile = BxDolService::call($this->_oConfig->sProfilesModule, 'prepare_fields', array($aFieldsProfile));
+
+        // check fields existence in Account
+        $oFormHelperAccount = BxDolService::call('system', 'forms_helper');
+        $oFormAccount = $oFormHelperAccount->getObjectFormAdd();
+        foreach ($aFieldsAccount as $sKey => $mValue) {
+            if (!$oFormAccount->isFieldExist($sKey))
+                unset($aFieldsAccount[$sKey]);
+        }
+
+        // check fields existence in Profile
+        if ('system' != $this->_oConfig->sProfilesModule && $oFormHelperProfile = BxDolService::call($this->_oConfig->sProfilesModule, 'forms_helper')) {
+            $oFormProfile = $oFormHelperProfile->getObjectFormAdd();
+            foreach ($aFieldsProfile as $sKey => $mValue) {
+                if (!$oFormProfile->isFieldExist($sKey))
+                    unset($aFieldsProfile[$sKey]);
+            }
+        }
+
+        // antispam check
+        $sErrorMsg = '';
+        $bSetPendingApproval = false;
+        bx_alert('account', 'check_join', 0, false, array('error_msg' => &$sErrorMsg, 'email' => $aFieldsAccount['email'], 'approve' => &$bSetPendingApproval));
+        if ($sErrorMsg)
+            return $sErrorMsg;
+
+        // check if user with the same email already exists
+        $oExistingAccount = BxDolAccount::getInstance($aFieldsAccount['email']);
+
+        // check redirect page
+        if ('join' == $this->_oConfig->sRedirectPage && !$oExistingAccount)
+            return array('remote_profile_info' => $aProfileInfo, 'profile_fields' => $aFieldsAccount, 'join_page_redirect' => true);
+
+        // create new profile
+        if ($oExistingAccount) {
+
+            if (!($oExistingProfile = BxDolProfile::getInstanceByAccount($oExistingAccount->id())))
+                return _t('_sys_txt_error_account_creation');
+
+            $iProfileId = $oExistingProfile->id();
+
+            $this->setLogged($iProfileId);
+        }
+        else {
+
+            // create account
+            $aFieldsAccount['password'] = genRndPwd();
+            $aFieldsAccount['email_confirmed'] = $this->_oConfig->isAlwaysConfirmEmail; 
+            if (!($iAccountId = $oFormAccount->insert($aFieldsAccount)))
+                return _t('_sys_txt_error_account_creation');
+
+            $isSetPendingApproval = $this->_oConfig->isAlwaysAutoApprove ? false : !(bool)getParam('sys_account_autoapproval');
+            $iAccountProfileId = $oFormHelperAccount->onAccountCreated ($iAccountId, $isSetPendingApproval, BX_PROFILE_ACTION_EXTERNAL);
+
+            // create profile
+            if (isset($oFormProfile) && $oFormProfile) {
+                
+                $aFieldsProfile['picture'] = $this->_processImage($aFieldsProfile, $iAccountProfileId, $oFormHelperProfile);
+                $_POST['picture'] = $aFieldsProfile['picture']; // set POST variable to correctly process images in processFiles method in form object
+
+                if (!($iContentId = $oFormProfile->insert($aFieldsProfile)))
+                    return _t('_sys_txt_error_account_creation');
+
+                $oFormHelperProfile->setAutoApproval($oFormHelperProfile->isAutoApproval() ? true : $this->_oConfig->isAlwaysAutoApprove);
+                if ($sErrorMsg = $oFormHelperProfile->onDataAddAfter ($iAccountId, $iContentId))
+                    return $sErrorMsg;
+                
+                $oProfile = BxDolProfile::getInstanceByAccount($iAccountId);
+                $iProfileId = $oProfile->id();
+            } 
+            else {
+                $iProfileId = $iAccountProfileId;
+            }
+
+            // send email with password
+            sendMailTemplate($this->_oConfig->sEmailTemplatePasswordGenerated, $iAccountId, $iProfileId, array('password' => $aFieldsAccount['password']), BX_EMAIL_SYSTEM);
+        }
+
+        // remember remote profile id for created member
+        $this ->_oDb->saveRemoteId($iProfileId, $aProfileInfo['id']);
+
+        // auto-friend members if they are already friends on remote site
+        if ($isAutoFriends && method_exists($this, '_makeFriends'))
+            $this->_makeFriends($iProfileId);
+
+        return array('remote_profile_info' => $aProfileInfo, 'profile_id' => $iProfileId, 'existing_profile' => $oExistingAccount ? true : false);
+    }
+
+    protected function _processImage($aFieldsProfile, $iAccountProfileId, $oFormHelperProfile)
+    {
+        if (!isset($aFieldsProfile['picture']) || !$aFieldsProfile['picture'])
+            return 0;
+
+        if (!($oStorage = $oFormHelperProfile->getObjectStorage()))
+            return 0;
+        
+        if (!($iFileId = $oStorage->storeFileFromUrl($aFieldsProfile['picture'], false, $iAccountProfileId)))
+            return 0;
+
+        return $iFileId;
+    }
+
+     /**
+      * Get join page
+      *
+      * @param $aProfileFields array
+      * @param $iRemoteProfileId remote profile id
+      * @return void
+      */
+    function _getJoinPage($aProfileFields, $iRemoteProfileId)
+    {
+        bx_import('BxDolSession');
+        $oSession = BxDolSession::getInstance();
+        $oSession->setValue($this->_oConfig->sSessionUid, $iRemoteProfileId);
+
+        $oPage = BxDolPage::getObjectInstanceByURI('create-account');
+
+        BxBaseAccountForms::$PROFILE_FIELDS = $aProfileFields;
+
+        $this->_oTemplate->getPage(false, $oPage->getCode());
+    }
+
+    /**
+     * get redirect URL
+     * 
+     * @param $iProfileId integer - profile ID
+     * @return string redirect URL
+     */
+    function _getRedirectUrl($iProfileId, $isExistingProfile = false)
+    {
+        $sRedirectUrl = $this->_oConfig->sDefaultRedirectUrl;
+
+        switch($this->_oConfig->sRedirectPage) {
+            case 'index':
+                $sRedirectUrl = BX_DOL_URL_ROOT;
+                break;
+
+            case 'settings':
+                if (!$isExistingProfile) { 
+                    $sRedirectUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-email');
+                    break;
+                }
+
+            case 'dashboard':
+            default:
+                $sRedirectUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=dashboard');
+                break;
+            }
+
+        return $sRedirectUrl;
+    }
+
+    protected function _redirect($sUrl, $iStatus = 302)
+    {
+        header("Location:{$sUrl}", true, $iStatus);
+        exit;
+    }
+}
+
+/** @} */

+ 210 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php

@@ -0,0 +1,210 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Create/edit entry form
+ */
+class BxBaseModGeneralFormEntry extends BxTemplFormView
+{
+    protected $MODULE;
+
+    protected $_oModule;
+
+    protected $_aMetatagsFieldsWithKeywords = array();
+    protected $_oMetatagsObject = null;
+    protected $_oMetatagsContentId = 0;
+
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if (isset($CNF['FIELD_ADDED']) && isset($this->aInputs[$CNF['FIELD_ADDED']])) {
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($CNF['FIELD_CHANGED']) && isset($this->aInputs[$CNF['FIELD_CHANGED']])) {
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']])) {
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['manual_input'] = true;
+        }
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if (!empty($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]) && isset($aValues[$CNF['FIELD_ID']]) && !empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) && $oMetatags->locationsIsEnabled())
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['value'] = $oMetatags->locationsString($aValues[$CNF['FIELD_ID']], false);
+
+        parent::initChecker ($aValues, $aSpecificValues);
+    }
+    
+    public function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($CNF['FIELD_AUTHOR']) && empty($aValsToAdd[$CNF['FIELD_AUTHOR']]))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = bx_get_logged_profile_id ();
+
+        if (isset($CNF['FIELD_ADDED']) && empty($aValsToAdd[$CNF['FIELD_ADDED']]))
+            $aValsToAdd[$CNF['FIELD_ADDED']] = time();
+
+        if (isset($CNF['FIELD_CHANGED']) && empty($aValsToAdd[$CNF['FIELD_CHANGED']]))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+
+        return parent::insert ($aValsToAdd, $isIgnore);
+    }
+
+    function update ($iContentId, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($CNF['FIELD_CHANGED']))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+            
+        return parent::update ($iContentId, $aValsToAdd, $aTrackTextFieldsChanges);
+    }
+
+    function delete ($iContentId, $aContentInfo = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // delete associated files
+
+        if (!empty($CNF['OBJECT_STORAGE'])) {
+            $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+            if ($oStorage)
+                $oStorage->queueFilesForDeletionFromGhosts($aContentInfo[$CNF['FIELD_AUTHOR']], $iContentId);
+        }
+
+        // delete associated objects data
+
+        if (!empty($CNF['OBJECT_VIEWS'])) {
+            $o = BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_VOTES'])) {
+            $o = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_COMMENTS'])) {
+            $o = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $oMetatags->onDeleteContent($iContentId);
+        }
+
+        // delete db record
+
+        return parent::delete($iContentId);
+    }
+
+    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);
+        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 addCssJs ()
+    {
+        if (!isset($this->aParams['view_mode']) || !$this->aParams['view_mode']) {
+            if (self::$_isCssJsAdded)
+                return;
+            $this->_oModule->_oTemplate->addCss('forms.css');
+            $this->_oModule->_oTemplate->addJs('modules/base/general/js/|forms.js');
+        }
+
+        return parent::addCssJs ();
+    }
+    
+    function genViewRowValue(&$aInput)
+    {
+        $s = parent::genViewRowValue($aInput);
+
+        if ($this->_oMetatagsObject && in_array($aInput['name'], $this->_aMetatagsFieldsWithKeywords) && $s)
+            $s = $this->_oMetatagsObject->keywordsParse($this->_oMetatagsContentId, $s);
+
+        return $s;
+    }
+
+    function setMetatagsKeywordsData($iId, $a, $o)
+    {
+        $this->_oMetatagsContentId = $iId;
+        $this->_aMetatagsFieldsWithKeywords = $a;
+        $this->_oMetatagsObject = $o;
+    }
+
+    function getContentOwnerProfileId ($iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // file owner must be author of the content or profile itself in case of profile based module
+        if ($iContentId) {
+            if ($this->_oModule instanceof iBxDolProfileService) {
+                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->MODULE);
+            }
+            else {
+                $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+                $oProfile = $aContentInfo ? BxDolProfile::getInstance($aContentInfo[$CNF['FIELD_AUTHOR']]) : null;
+            }
+            $iProfileId = $oProfile ? $oProfile->id() : bx_get_logged_profile_id();
+        }
+        else {
+            $iProfileId = bx_get_logged_profile_id();
+        }
+
+        return $iProfileId;
+    }
+
+}
+
+/** @} */

+ 343 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,343 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Entry forms helper functions
+ */
+class BxBaseModGeneralFormsEntryHelper extends BxDolProfileForms
+{
+    protected $_oModule;
+
+    public function __construct($oModule)
+    {
+        parent::__construct();
+        $this->_oModule = $oModule;
+    }
+
+    public function getObjectStorage()
+    {
+        return BxDolStorage::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_STORAGE']);
+    }
+
+    public function getObjectFormAdd ()
+    {
+        return BxDolForm::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY'], $this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormEdit ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+	public function getObjectFormView ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormDelete ()
+    {
+        return BxDolForm::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY'], $this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'], $this->_oModule->_oTemplate);
+    }
+
+    public function viewDataEntry ($iContentId)
+    {
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return MsgBox($sMsg);
+
+        return $this->_oModule->_oTemplate->entryText($aContentInfo);
+    }
+
+    public function addDataForm ()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedAdd()))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd();
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker();
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        // insert data into database
+        $aValsToAdd = array ();
+        $iContentId = $oForm->insert ($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_entry_creation'));
+        }
+
+        $sResult = $this->onDataAddAfter (getLoggedId(), $iContentId);
+        if ($sResult)
+            return $sResult;
+
+        // perform action
+        $this->_oModule->checkAllowedAdd(true);
+
+        // redirect
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $this->redirectAfterAdd($aContentInfo);
+    }
+    
+    protected function redirectAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function editDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedEdit($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $aSpecificValues = array();        
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->locationsIsEnabled())
+                $aSpecificValues = $oMetatags->locationGet($iContentId, empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+        $oForm->initChecker($aContentInfo, $aSpecificValues);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        // update data in the DB
+        $aTrackTextFieldsChanges = null;
+
+        $this->onDataEditBefore ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges);
+
+        if (!$oForm->update ($aContentInfo[$CNF['FIELD_ID']], array(), $aTrackTextFieldsChanges)) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_entry_update'));
+        }
+
+        $sResult = $this->onDataEditAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+        if ($sResult)
+            return $sResult;
+
+        // perform action
+        $this->_oModule->checkAllowedEdit($aContentInfo, true);
+
+        // redirect
+        $this->redirectAfterEdit($aContentInfo);
+    }
+
+    protected function redirectAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function deleteDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'];
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedDelete($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker($aContentInfo);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        if ($sError = $this->deleteData($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile, $oForm))
+            return MsgBox($sError);
+
+        // perform action
+        $this->_oModule->checkAllowedDelete($aContentInfo, true);
+
+        // redirect
+        $this->redirectAfterDelete($aContentInfo);
+    }
+
+    protected function redirectAfterDelete($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit($CNF['URL_HOME'], true, array(
+            'account_id' => getLoggedId(),
+            'profile_id' => bx_get_logged_profile_id(),
+        ));
+    }
+
+    /**
+     * Delete data entry
+     * @param $iContentId entry id
+     * @param $oForm optional content info array
+     * @param $aContentInfo optional content info array
+     * @param $oProfile optional content author profile
+     * @return error string on error or empty string on success
+     */
+    public function deleteData ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$aContentInfo || !$oProfile)
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if (!$aContentInfo)
+            return _t('_sys_txt_error_entry_is_not_defined');
+
+        if (!$oForm)
+            $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'], $this->_oModule->_oTemplate);
+
+        if (!$oForm->delete ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo))
+            return _t('_sys_txt_error_entry_delete');
+
+        if ($sResult = $this->onDataDeleteAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile))
+            return $sResult;
+
+        // create an alert
+        bx_alert($this->_oModule->getName(), 'deleted', $aContentInfo[$CNF['FIELD_ID']]);
+
+        return '';
+    }
+
+    public function viewDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if ($sMsg = $this->_processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile))
+            return MsgBox($sMsg);
+
+        // get form
+        $oForm = $this->getObjectFormView($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        // process metatags
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = $oMetatags->keywordsFields($aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']);
+                $oForm->setMetatagsKeywordsData($iContentId, $aFields, $oMetatags);
+            }
+        }        
+
+        // display profile
+        $oForm->initChecker($aContentInfo);
+        return $oForm->getCode();
+    }
+
+    protected function _processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return $sMsg;
+
+        return '';
+    }
+
+    public function onDataDeleteAfter ($iContentId, $aContentInfo, $oProfile)
+    {
+        return '';
+    }
+
+    public function onDataEditBefore ($iContentId, $aContentInfo, &$aTrackTextFieldsChanges)
+    {
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!empty($CNF['OBJECT_METATAGS'])) { // && isset($aTrackTextFieldsChanges['changed_fields'][$CNF['FIELD_TEXT']])) { // TODO: check if aTrackTextFieldsChanges works 
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled())
+                $oMetatags->keywordsAddAuto($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT']);
+            if ($oMetatags->locationsIsEnabled())
+                $oMetatags->locationsAddFromForm($aContentInfo[$CNF['FIELD_ID']], empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+
+        return '';
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        $this->_processMetas($iAccountId, $iContentId);
+
+        return '';
+    }
+
+    protected function _processMetas($iAccountId, $iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled() && $aContentInfo)
+                $oMetatags->keywordsAddAuto($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']);
+            if ($oMetatags->locationsIsEnabled() && $aContentInfo)
+                $oMetatags->locationsAddFromForm($aContentInfo[$CNF['FIELD_ID']], $CNF['FIELD_LOCATION_PREFIX']);
+        }
+    }
+}
+
+/** @} */

+ 936 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/classes/BxBaseModGeneralModule.php

@@ -0,0 +1,936 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+/**
+ * Base module class.
+ */
+class BxBaseModGeneralModule extends BxDolModule
+{
+    protected $_iProfileId;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+        $this->_iProfileId = bx_get_logged_profile_id();
+    }
+
+    // ====== ACTIONS METHODS
+
+    public function actionRss ()
+    {
+        $aArgs = func_get_args();
+        $this->_rss($aArgs);
+    }
+
+    // ====== SERVICE METHODS
+
+    public function serviceModuleIcon ()
+    {
+        return isset($this->_oConfig->CNF['ICON']) ? $this->_oConfig->CNF['ICON'] : '';
+    }
+
+    public function serviceGetSearchableFields ()
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        if (!isset($CNF['PARAM_SEARCHABLE_FIELDS']) || !isset($CNF['OBJECT_FORM_ENTRY']) || !isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']))
+            return array();
+
+        $aTextTypes = array('text', 'textarea');
+        $aTextFields = array();
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+        foreach ($oForm->aInputs as $r) {
+            if (in_array($r['type'], $aTextTypes))
+                $aTextFields[$r['name']] = $r['caption'];
+        }
+        return $aTextFields;
+    }
+    
+	public function serviceManageTools($sType = 'common')
+    {
+        $oGrid = BxDolGrid::getObjectInstance($this->_oConfig->getGridObject($sType));
+        if(!$oGrid)
+            return '';
+
+		$CNF = &$this->_oConfig->CNF;
+
+		$sMenu = '';
+		if(BxDolAcl::getInstance()->isMemberLevelInSet(192)) {
+			$oPermalink = BxDolPermalinks::getInstance();
+
+			$aMenuItems = array();
+			if(!empty($CNF['OBJECT_GRID_COMMON']) && !empty($CNF['T']['menu_item_manage_my']))
+				$aMenuItems[] = array('id' => 'manage-common', 'name' => 'manage-common', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_COMMON']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_my']), 'active' => 1);
+			if(!empty($CNF['OBJECT_GRID_ADMINISTRATION']) && !empty($CNF['T']['menu_item_manage_all']))
+				$aMenuItems[] = array('id' => 'manage-administration', 'name' => 'manage-administration', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_ADMINISTRATION']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_all']), 'active' => 1);
+
+			if(count($aMenuItems) > 1) {
+	            $oMenu = new BxTemplMenu(array(
+	            	'template' => 'menu_vertical.html', 
+	            	'menu_items' => $aMenuItems
+	            ), $this->_oTemplate);
+	            $oMenu->setSelected($this->_aModule['name'], 'manage-' . $sType);
+	            $sMenu = $oMenu->getCode();
+			}
+		}
+
+		if(!empty($CNF['OBJECT_MENU_SUBMENU'])) {
+			BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU'])->setSelected($this->_aModule['name'], $CNF['URI_MANAGE_COMMON']);
+		}
+
+        $this->_oTemplate->addCss(array('manage_tools.css'));
+        $this->_oTemplate->addJs(array('manage_tools.js'));
+        $this->_oTemplate->addJsTranslation(array('_sys_grid_search'));
+        return array(
+        	'content' => $this->_oTemplate->getJsCode('manage_tools', array('sObjNameGrid' => $this->_oConfig->getGridObject($sType))) . $oGrid->getCode(),
+        	'menu' => $sMenu
+        );
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+    	return 0;
+    }
+    
+    public function serviceGetMenuAddonManageToolsProfileStats()
+    {
+    	return 0;
+    }
+
+	/**
+     * Display featured entries
+     * @return HTML string
+     */
+    public function serviceBrowseFeatured ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('featured', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+	/**
+     * Display entries favored by a member
+     * @return HTML string
+     */
+    public function serviceBrowseFavorite ($iProfileId = 0, $aParams = array())
+    {
+        $oProfile = null;
+        if((int)$iProfileId)
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile && bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+        if(!$oProfile)
+            $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return '';
+
+        return $this->_serviceBrowse ('favorite', array_merge(array('user' => $oProfile->id()), $aParams), BX_DB_PADDING_DEF, true);
+    }
+
+    public function serviceFormsHelper ()
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        return new $sClass($this);
+    }
+
+    /**
+     * Add entry form
+     * @return HTML string
+     */
+    public function serviceEntityCreate ()
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addDataForm();
+    }
+
+    public function serviceEntityEdit ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId);
+    }
+
+    public function serviceEntityDelete ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('deleteDataForm', $iContentId);
+    }
+
+    public function serviceEntityTextBlock ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('viewDataEntry', $iContentId);
+    }
+
+    public function serviceEntityInfo ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId);
+    }
+
+	public function serviceEntityInfoFull ($iContentId = 0)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sDisplayName = 'OBJECT_FORM_ENTRY_DISPLAY_VIEW_FULL';
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId, !empty($CNF[$sDisplayName]) ? $CNF[$sDisplayName] : false);
+    }
+
+	public function serviceEntityInfoExtended ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryInfo', $iContentId);
+    }
+
+    /**
+     * Entry location info
+     */
+    public function serviceEntityLocation ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        return $this->_oTemplate->entryLocation ($iContentId);
+    }
+    
+    /**
+     * Delete content entry
+     * @param $iContentId content id 
+     * @return error message or empty string on success
+     */
+    public function serviceDeleteEntity ($iContentId, $sFuncDelete = 'deleteData')
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_oConfig->getClassPrefix() . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFuncDelete($iContentId);
+    }
+
+    /**
+     * Entry actions and social sharing block
+     */
+    public function serviceEntityAllActions ($iContentId = 0)
+    {
+        return $this->_oTemplate->entryAllActions('', $this->serviceEntitySocialSharing($iContentId));
+    }
+
+    /**
+     * Entry actions block
+     */
+    public function serviceEntityActions ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => $iContentId,
+        	'id_thumb' => !empty($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']]) ? $aContentInfo[$CNF['FIELD_THUMB']] : '',
+        	'title' => !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+        	'object_storage' => !empty($CNF['OBJECT_STORAGE']) ? $CNF['OBJECT_STORAGE'] : '',
+            'object_transcoder' => false,
+        	'object_view' => !empty($CNF['OBJECT_VIEWS']) ? $CNF['OBJECT_VIEWS'] : '',
+        	'object_vote' => !empty($CNF['OBJECT_VOTES']) ? $CNF['OBJECT_VOTES'] : '',
+        	'object_favorite' => !empty($CNF['OBJECT_FAVORITES']) ? $CNF['OBJECT_FAVORITES'] : '',
+            'object_feature' => !empty($CNF['OBJECT_FEATURED']) ? $CNF['OBJECT_FEATURED'] : '',
+        	'object_report' => !empty($CNF['OBJECT_REPORTS']) ? $CNF['OBJECT_REPORTS'] : '',
+        	'uri_view_entry' => !empty($CNF['URI_VIEW_ENTRY']) ? $CNF['URI_VIEW_ENTRY'] : '',
+        ));
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+    	$sEventPrivacy = $sModule . '_allow_view_event_to';
+		if(BxDolPrivacy::getObjectInstance($sEventPrivacy) === false)
+			$sEventPrivacy = '';
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_notifications_post', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'),
+                array('group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted'),
+                
+                array('group' => $sModule . '_comment', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'commentPost', 'module_name' => $sModule, 'module_method' => 'get_notifications_comment', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_comment', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'commentRemoved'),
+                
+                array('group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+				array('group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'),
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added'),
+                array('unit' => $sModule, 'action' => 'edited'),
+                array('unit' => $sModule, 'action' => 'deleted'),
+
+                array('unit' => $sModule, 'action' => 'commentPost'),
+                array('unit' => $sModule, 'action' => 'commentRemoved'),
+                
+                array('unit' => $sModule, 'action' => 'doVote'),
+                array('unit' => $sModule, 'action' => 'undoVote'),
+            )
+        );
+    }
+
+    /**
+     * Entry post for Notifications module
+     */
+    public function serviceGetNotificationsPost($aEvent)
+    {
+		$CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'entry_privacy' => '', //may be empty or not specified. In this case Public privacy will be used.
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+	/**
+     * Entry post comment for Notifications module
+     */
+    public function serviceGetNotificationsComment($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+		$oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+        if(!$oComment || !$oComment->isEnabled())
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'subentry_sample' => $CNF['T']['txt_sample_comment_single'],
+			'subentry_url' => $oComment->getViewUrl((int)$aEvent['subobject_id']),
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+	/**
+     * Entry post vote for Notifications module
+     */
+    public function serviceGetNotificationsVote($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+		$oVote = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+        if(!$oVote || !$oVote->isEnabled())
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'subentry_sample' => $CNF['T']['txt_sample_vote_single'],
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+    public function _serviceGetNotification($aEvent, $sLangKey)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if (!$oGroupProfile)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+        
+        $oProfile = BxDolProfile::getInstance((int)$aEvent['subobject_id']);
+        if (!$oProfile)
+            return false;
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $oGroupProfile->getUrl(),
+			'entry_caption' => $oGroupProfile->getDisplayName(),
+			'entry_author' => $oGroupProfile->id(),
+			'subentry_sample' => $oProfile->getDisplayName(),
+			'subentry_url' => $oProfile->getUrl(),
+			'lang_key' => $sLangKey,
+		);
+    }
+    
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_timeline_post', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'),
+                array('group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted')
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added'),
+                array('unit' => $sModule, 'action' => 'edited'),
+                array('unit' => $sModule, 'action' => 'deleted'),
+            )
+        );
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return '';
+
+        $CNF = &$this->_oConfig->CNF;
+        
+        //--- Views
+        $oViews = isset($CNF['OBJECT_VIEWS']) ? BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $aEvent['object_id']) : null;
+
+        $aViews = array();
+        if ($oViews && $oViews->isEnabled())
+            $aViews = array(
+                'system' => $CNF['OBJECT_VIEWS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['views']
+            );
+
+        //--- Votes
+        $oVotes = isset($CNF['OBJECT_VOTES']) ? BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $aEvent['object_id']) : null;
+
+        $aVotes = array();
+        if ($oVotes && $oVotes->isEnabled())
+            $aVotes = array(
+                'system' => $CNF['OBJECT_VOTES'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['votes']
+            );
+
+		//--- Reports
+        $oReports = isset($CNF['OBJECT_REPORTS']) ? BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $aEvent['object_id']) : null;
+
+        $aReports = array();
+        if ($oReports && $oReports->isEnabled())
+            $aReports = array(
+                'system' => $CNF['OBJECT_REPORTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['reports']
+            );
+
+        //--- Comments
+        $oCmts = isset($CNF['OBJECT_COMMENTS']) ? BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $aEvent['object_id']) : null;
+
+        $aComments = array();
+        if($oCmts && $oCmts->isEnabled())
+            $aComments = array(
+                'system' => $CNF['OBJECT_COMMENTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['comments']
+            );
+
+        return array(
+            'owner_id' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+        	'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+        	'sample_wo_article' => $CNF['T']['txt_sample_single'],
+    	    'sample_action' => isset($CNF['T']['txt_sample_single_action']) ? $CNF['T']['txt_sample_single_action'] : '',
+            'url' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]),
+            'content' => $this->_getContentForTimelinePost($aEvent, $aContentInfo), //a string to display or array to parse default template before displaying.
+            'date' => $aContentInfo[$CNF['FIELD_ADDED']],
+            'views' => $aViews,
+            'votes' => $aVotes,
+            'reports' => $aReports,
+            'comments' => $aComments,
+            'title' => '', //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    // ====== PERMISSION METHODS
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedBrowse ()
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // moderator and owner always have access
+        if ($aDataEntry[$CNF['FIELD_AUTHOR']] == $this->_iProfileId || $this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'view entry', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        // check privacy
+        if (!empty($CNF['OBJECT_PRIVACY_VIEW'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+            if ($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']]))
+                return _t('_sys_access_denied_to_private_content');
+        }
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'create entry', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator and owner always have access
+        if ($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId || $this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+        if ($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedSetMembership (&$aDataEntry, $isPerformAction = false)
+    {
+        // admin always has access
+        if (isAdmin())
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'set acl level', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEditAnyEntry ($isPerformAction = false)
+    {
+    	$aCheck = checkActionModule($this->_iProfileId, 'edit any entry', $this->getName(), $isPerformAction);
+    	if($aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+    		return CHECK_ACTION_RESULT_ALLOWED;
+
+    	return _t('_sys_txt_access_denied');
+    }
+
+    public function _serviceBrowse ($sMode, $aParams = false, $iDesignBox = BX_DB_PADDING_DEF, $bDisplayEmptyMsg = false, $bAjaxPaginate = true, $sClassSearchResult = 'SearchResult')
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse()))
+            return MsgBox($sMsg);
+
+        bx_import($sClassSearchResult, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClassSearchResult;
+        $o = new $sClass($sMode, $aParams);
+
+        $o->setDesignBoxTemplateId($iDesignBox);
+        $o->setDisplayEmptyMsg($bDisplayEmptyMsg);
+        $o->setAjaxPaginate($bAjaxPaginate);
+        $o->setUnitParams(array('context' => $sMode));
+
+        if ($o->isError)
+            return '';
+
+        if ($s = $o->processing())
+            return $s;
+        else
+            return '';
+    }
+
+	// ====== COMMON METHODS
+
+    public function getProfileId()
+    {
+    	return bx_get_logged_profile_id();
+    }
+
+	public function getProfileInfo($iUserId = 0)
+    {
+        $oProfile = $this->getObjectUser($iUserId);
+        $oAccount = $oProfile->getAccountObject();
+
+        return array(
+        	'id' => $oProfile->id(),
+            'name' => $oProfile->getDisplayName(),
+        	'email' => $oAccount->getEmail(),
+            'link' => $oProfile->getUrl(),
+            'icon' => $oProfile->getIcon(),
+        	'thumb' => $oProfile->getThumb(),
+        	'avatar' => $oProfile->getAvatar(),
+            'unit' => $oProfile->getUnit(),
+        	'active' => $oProfile->isActive(),
+        );
+    }
+    
+    public function getObjectUser($iUserId = 0)
+    {
+    	bx_import('BxDolProfile');
+        $oProfile = BxDolProfile::getInstance($iUserId);
+        if (!$oProfile) {
+            bx_import('BxDolProfileUndefined');
+            $oProfile = BxDolProfileUndefined::getInstance();
+        }
+
+        return $oProfile;
+    }
+
+    public function getObjectFavorite($sSystem = '', $iId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($sSystem) && !empty($CNF['OBJECT_FAVORITES']))
+            $sSystem = $CNF['OBJECT_FAVORITES'];
+
+        if(empty($sSystem))
+            return false;
+
+        $oFavorite = BxDolFavorite::getObjectInstance($sSystem, $iId, true, $this->_oTemplate);
+        if(!$oFavorite->isEnabled())
+            return false;
+
+        return $oFavorite;
+    }
+
+	public function getUserId()
+    {
+        return isLogged() ? bx_get_logged_profile_id() : 0;
+    }
+
+    public function getUserIp()
+    {
+        return getVisitorIP();
+    }
+
+    public function getUserInfo($iUserId = 0)
+    {
+        $oProfile = BxDolProfile::getInstance($iUserId);
+        if (!$oProfile)
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit()
+        );
+    }
+
+    // ====== PROTECTED METHODS
+    protected function _isModerator ($isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedEditAnyEntry ($isPerformAction);
+    }
+
+    protected function _serviceEntityForm ($sFormMethod, $iContentId = 0, $sDisplay = false)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFormMethod((int)$iContentId, $sDisplay);
+    }
+
+    protected function _serviceTemplateFunc ($sFunc, $iContentId, $sFuncGetContent = 'getContentInfoById')
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->$sFuncGetContent($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        return $this->_oTemplate->$sFunc($aContentInfo);
+    }
+
+    protected function _rss ($aArgs, $sClass = 'SearchResult')
+    {
+        $sMode = array_shift($aArgs);
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse())) {
+            $this->_oTemplate->displayAccessDenied ($sMsg);
+            exit;
+        }
+
+        $aParams = $this->_buildRssParams($sMode, $aArgs);
+
+        bx_import ($sClass, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClass;
+        $o = new $sClass($sMode, $aParams);
+
+        if ($o->isError)
+            $this->_oTemplate->displayPageNotFound ();
+        else
+            $o->outputRSS();
+
+        exit;
+    }
+
+    protected function _getContentForTimelinePost($aEvent, $aContentInfo)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+
+    	//--- Image(s)
+        $aImages = $this->_getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl);
+
+    	return array(
+    		'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+    		'sample_wo_article' => $CNF['T']['txt_sample_single'],
+    	    'sample_action' => isset($CNF['T']['txt_sample_single_action']) ? $CNF['T']['txt_sample_single_action'] : '',
+			'url' => $sUrl,
+			'title' => isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : 
+			(isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...') : ''),
+			'text' => isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '',
+			'images' => $aImages,
+		);
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sImage = '';
+        if(isset($CNF['FIELD_COVER']) && isset($aContentInfo[$CNF['FIELD_COVER']]) && $aContentInfo[$CNF['FIELD_COVER']]) {
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_COVER'])) {
+                $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_COVER']);
+            	if($oTranscoder)
+                    $sImage = $oTranscoder->getFileUrl($aContentInfo[$CNF['FIELD_COVER']]);
+            }
+
+            if($sImage == '' && !empty($CNF['OBJECT_IMAGES_TRANSCODER_THUMB'])) {
+                $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_THUMB']);
+            	if($oTranscoder)
+                    $sImage = $oTranscoder->getFileUrl($aContentInfo[$CNF['FIELD_COVER']]);
+            }
+        }
+
+        if($sImage == '' && isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) && $aContentInfo[$CNF['FIELD_THUMB']]) {
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_THUMB'])) {
+                $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_THUMB']);
+            	if($oTranscoder)
+                    $sImage = $oTranscoder->getFileUrl($aContentInfo[$CNF['FIELD_THUMB']]);
+            }
+
+            if($sImage == '' && !empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY'])) {
+            	$oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            	if($oTranscoder)
+                    $sImage = $oTranscoder->getFileUrl($aContentInfo[$CNF['FIELD_THUMB']]);
+            }
+        }
+
+        if(empty($sImage))
+            return array();
+
+        return array(
+		    array('url' => $sUrl, 'src' => $sImage),
+		);
+    }
+
+    protected function _entitySocialSharing ($iId, $aParams = array())
+    {
+        $sUrl = !empty($aParams['uri_view_entry']) ? BxDolPermalinks::getInstance()->permalink('page.php?i=' . $aParams['uri_view_entry'] . '&id=' . $iId) : '';
+        $sTitle = !empty($aParams['title']) ? $aParams['title'] : '';
+
+        //--- Comments
+        $sComments = '';
+        $oComments = !empty($aParams['object_comments']) ? BxTemplCmts::getObjectInstance($aParams['object_comments'], $iId) : false;
+        if ($oComments) {
+            $iNum = $oComments->getCommentsCountAll();
+            $sComments = $this->_oTemplate->parseHtmlByName('comments-item.html', array (
+                'url' => $sUrl . '#' . $oComments->getListAnchor(),
+                'bx_if:comments' => array (
+                    'condition' => $iNum,
+                    'content' => array (
+                        'num' => $iNum,
+                    ),
+                ),
+            ));
+        }
+
+        $aCustomParams = array();
+        $iIdThumb = isset($aParams['id_thumb']) ? (int)$aParams['id_thumb'] : 0;
+        $bSocialSharing = isset($aParams['social_sharing']) ? (bool)$aParams['social_sharing'] : true;
+        if ($iIdThumb && $bSocialSharing) {
+            if(!empty($aParams['object_transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($aParams['object_transcoder']);
+            else if(!empty($aParams['object_storage']))
+                $o = BxDolStorage::getObjectInstance($aParams['object_storage']);
+
+            $sImgUrl = $o ? $o->getFileUrlById($iIdThumb) : '';
+            if($sImgUrl)
+                $aCustomParams = array (
+                    'img_url' => $sImgUrl,
+                    'img_url_encoded' => rawurlencode($sImgUrl),
+                );
+        }
+
+        //TODO: Rebuild using menus engine when it will be ready for such elements like Vote, Share, etc.
+        //--- Views
+        $sViews = '';
+        $oViews = !empty($aParams['object_view']) ? BxDolView::getObjectInstance($aParams['object_view'], $iId) : false;
+        if ($oViews)
+            $sViews = $oViews->getElementBlock(array('show_do_view_as_button' => true));
+
+        //--- Votes
+        $sVotes = '';
+        $oVotes = !empty($aParams['object_vote']) ? BxDolVote::getObjectInstance($aParams['object_vote'], $iId) : false;
+        if ($oVotes)
+            $sVotes = $oVotes->getElementBlock(array('show_do_vote_as_button' => true));
+
+        //--- Favorite
+        $sFavorites = '';
+        $oFavorites = !empty($aParams['object_favorite']) ? BxDolFavorite::getObjectInstance($aParams['object_favorite'], $iId) : false;
+        if ($oFavorites)
+            $sFavorites = $oFavorites->getElementBlock(array('show_do_favorite_as_button' => true));
+
+        //--- Featured
+        $sFeatured = '';
+        $oFeatured = !empty($aParams['object_feature']) ? BxDolFeature::getObjectInstance($aParams['object_feature'], $iId) : false;
+        if ($oFeatured)
+            $sFeatured = $oFeatured->getElementBlock(array('show_do_feature_as_button' => true));
+
+        //--- Timeline Repost
+        $sRepost = '';
+        $iIdTimeline = isset($aParams['id_timeline']) ? (int)$aParams['id_timeline'] : $iId;
+        if ($iIdTimeline && BxDolRequest::serviceExists('bx_timeline', 'get_repost_element_block'))
+            $sRepost = BxDolService::call('bx_timeline', 'get_repost_element_block', array(bx_get_logged_profile_id(), $this->_aModule['name'], 'added', $iIdTimeline, array('show_do_repost_as_button' => true)));
+
+        //--- Report
+		$sReport = '';
+        $oReport = !empty($aParams['object_report']) ? BxDolReport::getObjectInstance($aParams['object_report'], $iId) : false;
+        if ($oReport)
+            $sReport = $oReport->getElementBlock(array('show_do_report_as_button' => true));
+
+        $sSocial = '';
+        if($bSocialSharing) {
+            $oSocial = BxDolMenu::getObjectInstance('sys_social_sharing');
+            $oSocial->addMarkers(array_merge(array(
+                'id' => $iId,
+            	'module' => $this->_aModule['name'],
+            	'url' => BX_DOL_URL_ROOT . $sUrl,
+            	'title' => $sTitle,
+            ), $aCustomParams));
+            $sSocial = $oSocial->getCode();
+        }
+
+        if(empty($sComments) && empty($sVotes) && empty($sFavorites) && empty($sRepost) && empty($sReport) && empty($sSocial))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('entry-share.html', array(
+            'comments' => $sComments,
+        	'view' => $sViews,
+            'vote' => $sVotes,
+            'favorite' => $sFavorites,
+            'feature' => $sFeatured,
+            'repost' => $sRepost,
+        	'report' => $sReport,
+            'social' => $sSocial,
+        ));
+        //TODO: Rebuild using menus engine when it will be ready for such elements like Vote, Repost, etc.
+    }
+}
+
+/** @} */

+ 37 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/template/css/main.css

@@ -0,0 +1,37 @@
+
+/* view entry */
+.bx-base-general-entity-info > p:first-child {
+    margin-top:0;
+}
+
+.bx-base-general-entity-share .bx-view,
+.bx-base-general-entity-share .bx-vote,
+.bx-base-general-entity-share .bx-favorite,
+.bx-base-general-entity-share .bx-feature,
+.bx-base-general-entity-share .bx-report,
+.bx-base-general-entity-share .bx-tl-repost,
+.bx-base-general-entity-share .bx-menu-btns-hor.bx-menu-object-sys_social_sharing {
+	position: relative;
+	float: none;
+	display: inline-block;
+	vertical-align: top;
+}
+
+
+/* unit */
+.bx-def-unit-live-search {
+    width:236px;
+    width:14.75;
+    overflow:hidden;
+}
+
+.bx-def-unit-live-search .bx-def-unit-info {
+    width:188px;
+    width:11.75;
+}
+
+.bx-def-unit-live-search .bx-def-unit-img {
+    background-size:cover;
+    background-repeat:no-repeat;
+    background-position:center center;
+}

+ 1 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/general/template/entry-share.html

@@ -0,0 +1 @@
+<div class="bx-base-general-entity-share bx-clearfix">__comments____view____vote____favorite____feature____repost____report____social__</div>

+ 35 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsFormEntry.php

@@ -0,0 +1,35 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Create/Edit Group Form.
+ */
+class BxBaseModGroupsFormEntry extends BxBaseModProfileFormEntry
+{
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($this->aInputs['initial_members']) && !isset($this->aInputs['initial_members']['value'])) {
+            $this->aInputs['initial_members']['value'] = isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']) && $CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE'] == $this->aParams['display'] ? array() : array(bx_get_logged_profile_id());
+        }
+    }
+
+    protected function genCustomInputInitialMembers ($aInput)
+    {
+        $aInput['ajax_get_suggestions'] = BX_DOL_URL_ROOT . "modules/?r=" . $this->_oModule->_oConfig->getUri() . "/ajax_get_initial_members";
+        return $this->genCustomInputUsernamesSuggestions($aInput);
+    }
+}
+
+/** @} */

+ 87 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsFormsEntryHelper.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    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Group profile forms functions
+ */
+class BxBaseModGroupsFormsEntryHelper extends BxBaseModProfileFormsEntryHelper
+{
+    public function __construct($oModule)
+    {
+        parent::__construct($oModule);
+    }
+
+    protected function _processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile)
+    {
+        $sMsg = parent::_processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile);
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+        if ($sMsg && $oPrivacy->isPartiallyVisible($aContentInfo[$this->_oModule->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]))
+            return '';
+
+        return $sMsg;
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        if ($s = parent::onDataAddAfter($iAccountId, $iContentId))
+            return $s;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->_oModule->_oConfig->getName())))
+            return '';
+
+        $this->inviteMembers ($oGroupProfile, bx_get('initial_members'));
+        
+        return '';
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        if ($s = parent::onDataEditAfter($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm))
+            return $s;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->_oModule->_oConfig->getName())))
+            return '';
+
+        $this->inviteMembers ($oGroupProfile, bx_get('initial_members'));
+
+        return '';
+    }
+    
+    public function onDataDeleteAfter ($iContentId, $aContentInfo, $oProfile)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->_oModule->_oConfig->getName());
+
+        if ($oGroupProfile)
+            $this->_oModule->_oDb->deleteAdminsByGroupId($oGroupProfile->id());
+
+        if (isset($CNF['OBJECT_CONNECTIONS']) && $oGroupProfile && ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            $oConnection->onDeleteInitiatorAndContent($oGroupProfile->id());
+
+        return '';
+    }
+
+    protected function inviteMembers ($oGroupProfile, $aInitialProfiles)
+    {
+        if (!$aInitialProfiles)
+            return;
+
+        // insert invited members, so they will join without confirmation
+        foreach ($aInitialProfiles as $iProfileId) {
+            if (!($oProfile = BxDolProfile::getInstance($iProfileId)))
+                continue;
+            $this->_oModule->serviceAddMutualConnection ($oGroupProfile->id(), $oProfile->id(), true);            
+        }
+    }
+}
+
+/** @} */

+ 25 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsGridAdministration.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    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsGridAdministration extends BxBaseModProfileGridAdministration
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+    }
+
+    protected function _getCellName($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_getCellFullname($mixedValue, $sKey, $aField, $aRow);
+    }
+}
+
+/** @} */

+ 29 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsGridCommon.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    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsGridCommon extends BxBaseModGroupsGridAdministration
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_sManageType = BX_DOL_MANAGE_TOOLS_COMMON;
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+		$this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `tp`.`account_id`=?", getLoggedId());
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+}
+
+/** @} */

+ 503 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,503 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Groups profiles module.
+ */
+class BxBaseModGroupsModule extends BxBaseModProfileModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    /**
+     * Get possible recipients for start conversation form
+     */
+    public function actionAjaxGetInitialMembers ()
+    {
+        $sTerm = bx_get('term');
+
+        $a = BxDolService::call('system', 'profiles_search', array($sTerm), 'TemplServiceProfiles');
+
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo(json_encode($a));
+    }
+
+    /**
+     * Check if this module entry can be used as profile
+     */
+    public function serviceActAsProfile ()
+    {
+        return false;
+    }
+
+    /**
+     * Check if this module is group profile
+     */
+    public function serviceIsGroupProfile ()
+    {
+        return true;
+    }
+
+    /**
+     * check if provided profile is member if the group 
+     */ 
+    public function serviceIsFan ($iGroupProfileId, $iProfileId = false) 
+    {
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        return $this->isFan($oGroupProfile->getContentId(), $iProfileId);
+    }
+
+    /**
+     * Delete profile from fans and admins tables
+     * @param $iProfileId profile id 
+     */
+    public function serviceDeleteProfileFromFansAndAdmins ($iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $this->_oDb->deleteAdminsByProfileId($iProfileId);
+
+        if (isset($CNF['OBJECT_CONNECTIONS']) && ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            $oConnection->onDeleteInitiatorAndContent($iProfileId);
+    }
+
+    /**
+     * Reset group's author for particular group
+     * @param $iContentId group id 
+     * @return false of error, or number of updated records on success
+     */
+    public function serviceReassignEntityAuthor ($iContentId)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById((int)$iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+
+        $aAdmins = $this->_oDb->getAdmins($oGroupProfile->id());
+
+        return $this->_oDb->updateAuthorById($iContentId, $aAdmins ? array_pop($aAdmins) : 0);
+    }
+
+    /**
+     * Reset group's author when author profile is deleted
+     * @param $iProfileId profile id 
+     * @return number of changed items
+     */
+    public function serviceReassignEntitiesByAuthor ($iProfileId)
+    {
+        $a = $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+        if (!$a)
+            return 0;
+
+        $iCount = 0;
+        foreach ($a as $aContentInfo)
+            $iCount += ('' == $this->serviceReassignEntityAuthor($aContentInfo[$this->_oConfig->CNF['FIELD_ID']]) ? 1 : 0);
+
+        return $iCount;
+    }
+
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $aFieldsProfile[$CNF['FIELD_NAME']] = $aFieldsProfile['name'];
+        $aFieldsProfile[$CNF['FIELD_TEXT']] = isset($aFieldsProfile['description']) ? $aFieldsProfile['description'] : '';
+        unset($aFieldsProfile['name']);
+        unset($aFieldsProfile['description']);
+        return $aFieldsProfile;
+    }
+
+    public function serviceOnRemoveConnection ($iGroupProfileId, $iInitiatorId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        list ($iProfileId, $iGroupProfileId, $oGroupProfile) = $this->_prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId);
+        if (!$oGroupProfile)
+            return false;
+
+        $this->_oDb->fromAdmins($iGroupProfileId, $iProfileId);
+
+        if ($oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions'))
+            $oConn->removeConnection($iProfileId, $iGroupProfileId);
+    }
+
+    public function serviceAddMutualConnection ($iGroupProfileId, $iInitiatorId, $iIgnoreJoinConfirmation = false)
+    {        
+        $CNF = &$this->_oConfig->CNF;
+
+        list ($iProfileId, $iGroupProfileId, $oGroupProfile) = $this->_prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId);
+        if (!$oGroupProfile)
+            return false;
+
+        if (!($aContentInfo = $this->_oDb->getContentInfoById((int)BxDolProfile::getInstance($iGroupProfileId)->getContentId())))
+            return false;
+
+        if (!($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            return false;
+
+
+
+        $sEntryTitle = $aContentInfo[$CNF['FIELD_NAME']];
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+
+        // send invitation to the group 
+        if ($iIgnoreJoinConfirmation && !$oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id()) && !$oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId) && bx_get_logged_profile_id() != $iProfileId) {
+
+            bx_alert($this->getName(), 'join_invitation', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+        // send notification to group's admins that new connection is pending confirmation 
+        elseif (!$iIgnoreJoinConfirmation && $oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id()) && !$oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId) && $aContentInfo['join_confirmation']) {
+
+            bx_alert($this->getName(), 'join_request', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+        // send notification that join request was accepted 
+        else if (!$iIgnoreJoinConfirmation && $oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id(), true) && $oGroupProfile->getModule() != $this->getName() && bx_get_logged_profile_id() != $iProfileId) {
+            
+            bx_alert($this->getName(), 'join_request_accepted', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+
+        // new fan was added
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true)) {
+            bx_alert($this->getName(), 'fan_added', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+            return false;
+        }
+
+        // don't automatically add back connection (mutual) if group requires manual join confirmation
+        if (!$iIgnoreJoinConfirmation && $aContentInfo['join_confirmation'])
+            return false;
+
+        // check if connection already exists
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true) || $oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        if (!$oConnection->addConnection($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        return true;
+    }
+
+    public function serviceFansTable ()
+    {
+        $oGrid = BxDolGrid::getObjectInstance($this->_oConfig->CNF['OBJECT_GRID_CONNECTIONS']);
+        if (!$oGrid)
+            return false;
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceFans ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($oGroupProfile->id(), $this->_oConfig->CNF['OBJECT_CONNECTIONS'], BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+        return $s;
+    }
+
+    public function serviceBrowseJoinedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('joined_entries', array('joined_profile' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceEntityInvite ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']);
+    }
+    
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if (!$oGroupProfile)
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => $iContentId,
+            'id_thumb' => 0,
+            'title' => $oGroupProfile->getDisplayName(),
+            'object_storage' => false,
+            'object_transcoder' => false,
+            'object_vote' => $CNF['OBJECT_VOTES'],
+        	'object_favorite' => $CNF['OBJECT_FAVORITES'],
+        	'object_feature' => $CNF['OBJECT_FEATURED'],
+        	'object_report' => $CNF['OBJECT_REPORTS'],
+            'uri_view_entry' => $CNF['URI_VIEW_ENTRY']
+        ));
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module'),
+                array('group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'),
+
+                array('group' => $sModule . '_fan_added', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'fan_added', 'module_name' => $sModule, 'module_method' => 'get_notifications_fan_added', 'module_class' => 'Module'),
+
+                array('group' => $sModule . '_join_request', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'join_request', 'module_name' => $sModule, 'module_method' => 'get_notifications_join_request', 'module_class' => 'Module', 'module_event_privacy' => $this->_oConfig->CNF['OBJECT_PRIVACY_VIEW_NOTIFICATION_EVENT']),
+                
+                array('group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module'),
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'doVote'),
+                array('unit' => $sModule, 'action' => 'undoVote'),
+                array('unit' => $sModule, 'action' => 'fan_added'),
+                array('unit' => $sModule, 'action' => 'join_request'),
+                array('unit' => $sModule, 'action' => 'timeline_post_common'),
+            )
+        );
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsJoinRequest($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, $this->_oConfig->CNF['T']['txt_ntfs_join_request']);
+    }
+    
+	/**
+     * Notification about new member in the group
+     */
+    public function serviceGetNotificationsFanAdded($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, $this->_oConfig->CNF['T']['txt_ntfs_fan_added']);
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent)
+    {
+        $a = parent::serviceGetTimelinePost($aEvent);
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+
+        $a['content']['url'] = $oGroupProfile->getUrl();
+        $a['content']['title'] = $oGroupProfile->getDisplayName();
+        
+        return $a;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = parent::checkAllowedViewCoverImage($aDataEntry)) && $oPrivacy->isPartiallyVisible($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        
+        return $sMsg;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewProfileImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedViewCoverImage($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedPost ($aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose (&$aDataEntry, $isPerformAction = false)
+    {
+        if (!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return _t('_sys_txt_access_denied');
+        
+        return parent::checkAllowedCompose ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if (!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]) && isLogged())
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, false);
+    }
+
+    public function checkAllowedManageAdmins ($mixedDataEntry, $isPerformAction = false)
+    {
+        if (is_array($mixedDataEntry)) {
+            $aDataEntry = $mixedDataEntry;
+        }
+        else {
+            $oGroupProfile = BxDolProfile::getInstance((int)$mixedDataEntry);
+            $aDataEntry = $oGroupProfile && $this->getName() == $oGroupProfile->getModule() ? $this->_oDb->getContentInfoById($oGroupProfile->getContentId()) : array();
+        }
+
+        return parent::checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedInvite ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+    
+    public function checkAllowedChangeCover ($aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedChangeCover ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($oGroupProfile && $this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedDelete ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedSubscribeAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if (!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return _t('_sys_txt_access_denied');
+
+        return parent::checkAllowedSubscribeAdd ($aDataEntry, $isPerformAction);
+    }
+
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        $sResult = $this->checkAllowedView($aDataEntry);
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== $sResult && !$oPrivacy->isPartiallyVisible($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]))
+            return $sResult;
+
+        return parent::_checkAllowedConnect ($aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap);
+    }
+
+    public function isFan ($iContentId, $iProfileId = false) 
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+
+        return $oGroupProfile && ($oConnection = BxDolConnection::getObjectInstance($this->_oConfig->CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aContentInfo[$CNF['FIELD_PICTURE']]))
+            return array();
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+
+        return array(
+		    array('url' => $sUrl, 'src' => $oGroupProfile->getPicture()),
+		);
+    }
+
+    protected function _prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId)
+    {
+        if (!($oGroupProfile = BxDolProfile::getInstance($iGroupProfileId)))
+            return array(0, 0, null);
+
+        if ($oGroupProfile->getModule() == $this->getName()) {
+            $iProfileId = $iInitiatorId;
+            $iGroupProfileId = $oGroupProfile->id();
+        } else {
+            $iProfileId = $oGroupProfile->id();
+            $iGroupProfileId = $iInitiatorId;
+        }
+
+        return array($iProfileId, $iGroupProfileId, $oGroupProfile);
+    }
+}
+
+/** @} */

+ 128 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsModule.php

@@ -0,0 +1,128 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseNotifications Base classes for Notifications like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_BASE_MOD_NTFS_HANDLER_TYPE_INSERT', 'insert');
+define('BX_BASE_MOD_NTFS_HANDLER_TYPE_UPDATE', 'update');
+define('BX_BASE_MOD_NTFS_HANDLER_TYPE_DELETE', 'delete');
+
+define('BX_BASE_MOD_NTFS_TYPE_OWNER', 'owner');
+define('BX_BASE_MOD_NTFS_TYPE_OBJECT_OWNER', 'object_owner');
+define('BX_BASE_MOD_NTFS_TYPE_CONNECTIONS', 'connections');
+define('BX_BASE_MOD_NTFS_TYPE_PUBLIC', 'public');
+
+/**
+ * Base module class.
+ */
+class BxBaseModNotificationsModule extends BxBaseModGeneralModule
+{
+	public $_iOwnerId;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->_oConfig->init($this->_oDb);
+
+        $this->_iOwnerId = 0;
+    }
+
+	public function serviceAddHandlers($sModuleUri)
+    {
+        $this->_updateModuleData('add_handlers', $sModuleUri);
+    }
+
+    public function serviceDeleteHandlers($sModuleUri)
+    {
+        $this->_updateModuleData('delete_handlers', $sModuleUri);
+    }
+
+	public function serviceDeleteModuleEvents($sModuleUri)
+    {
+        $this->_updateModuleData('delete_module_events', $sModuleUri);
+    }
+
+    function serviceGetActionsChecklist()
+    {
+    	$sLangPrefix = $this->_oConfig->getPrefix('language');
+        $aHandlers = $this->_oConfig->getHandlers();
+
+        $aResults = array();
+        foreach($aHandlers as $aHandler) {
+            if($aHandler['type'] != BX_BASE_MOD_NTFS_HANDLER_TYPE_INSERT)
+                continue;
+
+			$_sUnit = '_' . $aHandler['alert_unit'];
+			$sUnit = _t($_sUnit);
+			if(strcmp($_sUnit, $sUnit) === 0)
+				$sUnit = _t($sLangPrefix . '_alert_module_' . $aHandler['alert_unit']);
+
+			$sAction = '';
+            if(!empty($aHandler['alert_action'])) {
+            	$_sAction = '_' . $aHandler['alert_unit'] . '_alert_action_' . $aHandler['alert_action'];
+            	$sAction = _t($_sAction);
+            	if(strcmp($_sAction, $sAction) === 0)
+					$sAction = _t($sLangPrefix . '_alert_action_' . $aHandler['alert_action']);
+
+            	$sAction = ' (' . $sAction . ')';
+            }
+
+            $aResults[$aHandler['id']] = $sUnit . $sAction;
+        }
+
+        asort($aResults);
+        return $aResults;
+    }
+
+    public function isAllowedView($aEvent, $bPerform = false)
+    {
+		return true;
+    }
+
+    public function getOwnerId()
+    {
+    	return $this->_iOwnerId;
+    }
+
+	protected function _updateModuleData($sAction, $sModuleUri)
+    {
+    	$sMethod = $this->_oConfig->getHandlersMethod();
+
+        $aModule = $this->_oDb->getModuleByUri($sModuleUri);
+		if(!BxDolRequest::serviceExists($aModule, $sMethod))
+        	return;
+
+		$aData = BxDolService::call($aModule['name'], $sMethod);
+		if(empty($aData) || !is_array($aData))
+        	return;
+
+		switch($sAction) {
+			case 'add_handlers':
+				$this->_oDb->insertData($aData);
+				BxDolAlerts::cacheInvalidate();
+
+				$this->_oDb->activateModuleEvents($aData, true);
+				break;
+
+			case 'delete_handlers':
+				$this->_oDb->deleteData($aData);
+				BxDolAlerts::cacheInvalidate();
+
+				$this->_oDb->activateModuleEvents($aData, false);
+				break;
+
+			case 'delete_module_events':
+				$this->_oDb->deleteModuleEvents($aData);
+				break;
+		}
+    }
+}
+
+/** @} */

+ 49 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsResponse.php

@@ -0,0 +1,49 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseNotifications Base classes for Notifications like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModNotificationsResponse extends BxDolAlertsResponse
+{
+    protected $_oModule;
+
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+	protected function _getObjectOwnerId($aExtras)
+    {
+        return is_array($aExtras) && isset($aExtras['object_author_id']) ? (int)$aExtras['object_author_id'] : 0;
+    }
+
+    protected function _getObjectPrivacyView($aExtras)
+    {
+        return is_array($aExtras) && isset($aExtras['privacy_view']) ? (int)$aExtras['privacy_view'] : $this->_oModule->_oConfig->getPrivacyViewDefault('object');
+    }
+
+	protected function _getSubObjectId($aExtras)
+    {
+    	if(is_array($aExtras) && isset($aExtras['comment_id']))
+    		return (int)$aExtras['comment_id'];
+
+   		if(is_array($aExtras) && isset($aExtras['vote_id']))
+    		return (int)$aExtras['vote_id'];
+
+        if(is_array($aExtras) && isset($aExtras['repost_id']))
+    		return (int)$aExtras['repost_id'];
+
+        if(is_array($aExtras) && isset($aExtras['notification_subobject_id']))
+            return (int)$aExtras['notification_subobject_id'];
+                
+        return 0;
+    }
+}
+
+/** @} */

+ 100 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/notifications/classes/BxBaseModNotificationsTemplate.php

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

+ 162 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/payment/classes/BxBaseModPaymentConfig.php

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

+ 168 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/profile/classes/BxBaseModProfileAlertsResponse.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    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModProfileAlertsResponse extends BxBaseModGeneralAlertsResponse
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function response($oAlert)
+    {
+        parent::response($oAlert);
+
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        // update picture field id when file is deleted
+        if ($CNF['OBJECT_STORAGE'] == $oAlert->sUnit && 'file_deleted' == $oAlert->sAction) {
+            $this->_oModule->_oDb->resetContentPictureByFileId($oAlert->iObject, $CNF['FIELD_PICTURE']);
+        }
+        if ($CNF['OBJECT_STORAGE_COVER'] == $oAlert->sUnit && 'file_deleted' == $oAlert->sAction) {
+            $this->_oModule->_oDb->resetContentPictureByFileId($oAlert->iObject, $CNF['FIELD_COVER']);
+        }        
+
+        // connection events
+        if ($oAlert->sUnit == 'sys_profiles_friends' && $oAlert->sAction == 'connection_added') {
+            if((int)$oAlert->aExtras['mutual'] == 0)
+                $this->sendMailFriendRequest($oAlert);
+        }
+
+        // re-translate timeline alert for timeline in this module for posts made by other profiles
+        if ('bx_timeline' == $oAlert->sUnit && 'post_common' == $oAlert->sAction && ($oGroupProfile = BxDolProfile::getInstance($oAlert->aExtras['object_author_id'])) && $oGroupProfile->getModule() == $this->_oModule->getName() && $oGroupProfile->id() != $oAlert->iSender) {            
+            $aContentInfo = $this->_oModule->serviceGetContentInfoById($oGroupProfile->getContentId());
+            bx_alert($this->_oModule->getName(), 'timeline_post_common', $aContentInfo[$CNF['FIELD_ID']], $oGroupProfile->id(), array('content' => $aContentInfo, 'group_profile' => $oGroupProfile->id(), 'profile' => $oAlert->iSender, 'notification_subobject_id' => $oAlert->iSender, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+        }
+ 
+        if ($this->MODULE != $oAlert->sUnit)
+            return;
+
+        // timeline events to override permissions
+        switch ($oAlert->sAction) {
+        case 'timeline_view':
+            $this->processTimelineView($oAlert, $oAlert->iObject);
+            break;
+
+        case 'timeline_comment':
+        case 'timeline_report':
+        case 'timeline_vote':
+            $this->processTimelineEventsBoolResult($oAlert, $oAlert->iObject);
+            break;
+
+        case 'timeline_post':
+            $this->processTimelineEventsCheckResult($oAlert, $oAlert->iObject);
+            break;
+
+        case 'timeline_delete':
+            $this->processTimelineEventsCheckResult($oAlert, $oAlert->iObject, 'checkAllowedEdit');
+            break;
+
+        case 'timeline_share':
+            $this->processTimelineShare($oAlert, $oAlert->iObject);
+            break;
+        }
+
+    }
+
+    protected function sendMailFriendRequest ($oAlert)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if(empty($CNF['EMAIL_FRIEND_REQUEST']) || empty($CNF['URI_VIEW_FRIENDS']))
+            return;
+
+        $iRecipient = $oAlert->aExtras['content'];
+        $oRecipient = BxDolProfile::getInstance($iRecipient);
+        if($oRecipient->getModule() != $this->MODULE)
+            return;
+
+        $iSender = $oAlert->aExtras['initiator'];
+        $oSender = BxDolProfile::getInstance($iSender);
+        sendMailTemplate($CNF['EMAIL_FRIEND_REQUEST'], 0, $iRecipient, array(
+            'SenderUrl' => $oSender->getUrl(),
+            'SenderDisplayName' => $oSender->getDisplayName(),
+            'FriendsLink' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_FRIENDS'] . '&profile_id=' . $iRecipient),
+        ), BX_EMAIL_NOTIFY);
+    }
+
+    protected function processTimelineView ($oAlert, $iGroupProfileId, $bDisableOwnerActions = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if (!$oGroupProfile) 
+            return;
+
+        $bDisableOwnerActions = !BxDolService::call($oGroupProfile->getModule(), 'act_as_profile');
+
+        $aContentInfo = $this->_oModule->serviceGetContentInfoById($oGroupProfile->getContentId());
+        if (CHECK_ACTION_RESULT_ALLOWED !== $this->_oModule->checkAllowedView($aContentInfo)) {
+            $oAlert->aExtras['override_content'] = MsgBox(_t('_sys_access_denied_to_private_content'));
+        }
+
+        if ($bDisableOwnerActions && isset($oAlert->aExtras['menu'])) {
+            foreach ($oAlert->aExtras['menu'] as $i => $r) {
+                if ('timeline-view-owner' == $r['id'] || 'timeline-view-other'  == $r['id'])
+                    unset($oAlert->aExtras['menu'][$i]);
+            }
+        }
+    }
+
+    protected function processTimelineEventsCheckResult ($oAlert, $iGroupProfileId, $sFunc = 'checkAllowedPost')
+    {
+        if ($oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return;
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if (!$oGroupProfile) 
+            return;
+
+        $aContentInfo = $this->_oModule->serviceGetContentInfoById($oGroupProfile->getContentId());
+        if (CHECK_ACTION_RESULT_ALLOWED === ($s = $this->_oModule->$sFunc($aContentInfo))) {
+            $oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+        } 
+        else {
+            $oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] = CHECK_ACTION_MESSAGE_NOT_ALLOWED;
+            $oAlert->aExtras['check_result'][CHECK_ACTION_MESSAGE] = $s;
+        }
+    }
+
+    protected function processTimelineEventsBoolResult ($oAlert, $iGroupProfileId, $sFunc = 'checkAllowedPost')
+    {
+        if (isAdmin() || !$oAlert->aExtras['result'])
+            return;
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if (!$oGroupProfile) 
+            return;
+
+        $aContentInfo = $this->_oModule->serviceGetContentInfoById($oGroupProfile->getContentId());
+        $oAlert->aExtras['result'] = (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->$sFunc($aContentInfo)) ? true : false;
+    }
+
+    protected function processTimelineShare ($oAlert, $iGroupProfileId)
+    {
+        if ($oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return;
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if (!$oGroupProfile) 
+            return;
+
+        $aContentInfo = $this->_oModule->serviceGetContentInfoById($oGroupProfile->getContentId());
+        if (BX_DOL_PG_ALL == $aContentInfo[$this->_oModule->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]) {
+            $oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+        }
+        else {
+            $oAlert->aExtras['check_result'][CHECK_ACTION_RESULT] = CHECK_ACTION_MESSAGE_NOT_ALLOWED;
+            $oAlert->aExtras['check_result'][CHECK_ACTION_MESSAGE] = _t('_sys_access_denied_to_private_content');
+        }
+    }
+}
+
+/** @} */

+ 661 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/profile/classes/BxBaseModProfileModule.php

@@ -0,0 +1,661 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Base class for profile modules.
+ */
+class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolProfileService
+{
+    protected $_iAccountId;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+        $this->_iAccountId = getLoggedId();
+    }
+
+    public function actionDeleteProfileImg($iFileId, $iContentId, $sFieldPicture) 
+    {
+        $aResult = array();
+        $CNF = &$this->_oConfig->CNF;
+
+        $oSrorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+        if (!($aFile = $oSrorage->getFile((int)$iFileId)) || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)) || $aContentInfo[$sFieldPicture] != (int)$iFileId)
+            $aResult = array('error' => 1, 'msg' => _t('_sys_storage_err_file_not_found'));
+
+        $oAccountProfile = BxDolProfile::getInstanceAccountProfile();
+        if ($oAccountProfile)
+            $iAccountProfileId = $oAccountProfile->id();
+
+        if ((!$aResult && !isLogged()) || (!$aResult && $aFile['profile_id'] != $iAccountProfileId && !$this->_isModerator()))           
+            $aResult = array('error' => 2, 'msg' => _t('_Access denied'));
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+
+        if (!$aResult && !$oForm->_deleteFile($iContentId, $sFieldPicture, (int)$iFileId, true))
+            $aResult = array('error' => 3, 'msg' => _t('_Failed'));
+        elseif (!$aResult)            
+            $aResult = array('error' => 0, 'msg' => '');
+
+        header('Content-Type: application/json; charset=utf-8');
+        echo json_encode($aResult);
+    }
+
+    // ====== SERVICE METHODS
+
+	public function servicePrivateProfileMsg()
+    {
+        return MsgBox(_t('_sys_access_denied_to_private_content'));
+    }
+    
+	public function serviceGetContentInfoById($iContentId)
+    {
+        return $this->_oDb->getContentInfoById((int)$iContentId);
+    }
+
+	public function serviceGetMenuAddonManageTools()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'perofileStatus' => BX_PROFILE_STATUS_PENDING
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+	public function serviceGetMenuAddonManageToolsProfileStats()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'account_id' => getLoggedId(),
+        	'perofileStatus' => ''
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+    public function serviceGetMenuAddonFavoritesProfileStats()
+    {
+    	bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass('favorite', array('user' => bx_get_logged_profile_id()));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+    }
+
+    public function serviceGetSubmenuObject ()
+    {
+        return $this->_oConfig->CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+    }
+
+    public function serviceGetMenuSetNameForMenuTrigger ($sMenuTriggerName)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if ($CNF['TRIGGER_MENU_PROFILE_VIEW_SUBMENU'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+        elseif ($CNF['TRIGGER_MENU_PROFILE_VIEW_ACTIONS'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'];
+        return '';
+    }
+
+	public function serviceGetPageObjectForPageTrigger ($sPageTriggerName)
+    {
+        if (isset($this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY']) && $this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY'] == $sPageTriggerName)
+        	return $this->_oConfig->CNF['OBJECT_PAGE_VIEW_ENTRY'];
+
+        return '';
+    }
+
+    public function serviceProfilesSearch ($sTerm, $iLimit)
+    {
+        $aRet = array();
+        $a = $this->_oDb->searchByTerm($sTerm, $iLimit);
+        foreach ($a as $r)
+            $aRet[] = array ('label' => $this->serviceProfileName($r['content_id']), 'value' => $r['profile_id']);
+        return $aRet;
+    }
+
+    public function serviceProfileUnit ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('unit', $iContentId);
+    }
+
+    public function serviceProfilePicture ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlPicture', $iContentId);
+    }
+
+    public function serviceProfileAvatar ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlAvatar', $iContentId);
+    }
+
+    public function serviceProfileEditUrl ($iContentId)
+    {
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $this->_oConfig->CNF['URI_EDIT_ENTRY'] . '&id=' . $iContentId);
+    }
+
+    public function serviceProfileThumb ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('thumb', $iContentId);
+    }
+
+    public function serviceProfileIcon ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('icon', $iContentId);
+    }
+
+    public function serviceProfileName ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        return bx_process_output($aContentInfo[$this->_oConfig->CNF['FIELD_NAME']]);
+    }
+
+    public function serviceProfileCreateUrl ($bAbsolute = true)
+    {
+    	$CNF = $this->_oConfig->CNF;
+    	if(empty($CNF['URL_CREATE']))
+    		return false;
+
+    	return $bAbsolute ? BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($CNF['URL_CREATE']) : $CNF['URL_CREATE'];
+    }
+
+    public function serviceProfileUrl ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        $CNF = $this->_oConfig->CNF;
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        return $aFieldsProfile;
+    }
+
+    public function serviceFormsHelper ()
+    {
+        return parent::serviceFormsHelper ();
+    }
+
+    public function serviceEntityCreate ()
+    {
+	    BxDolInformer::getInstance($this->_oTemplate)->setEnabled(false);
+
+	    return parent::serviceEntityCreate ();
+    }
+
+    public function serviceActAsProfile ()
+    {
+        return true;
+    }
+
+    public function serviceBrowseRecentProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('recent', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseActiveProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('active', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseTopProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('top', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+    
+	public function serviceBrowseOnlineProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('online', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseConnections ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iDesignBox = BX_DB_PADDING_DEF, $iProfileId2 = 0)
+    {
+        return $this->_serviceBrowse (
+            'connections',
+            array(
+                'object' => $sObjectConnections,
+                'type' => $sConnectionsType,
+                'mutual' => $iMutual,
+                'profile' => (int)$iProfileId,
+                'profile2' => (int)$iProfileId2),
+            $iDesignBox
+        );
+    }
+
+    public function serviceBrowseConnectionsQuick ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iProfileId2 = 0)
+    {
+        // get connections object
+        $oConnection = BxDolConnection::getObjectInstance($sObjectConnections);
+        if (!$oConnection)
+            return '';
+
+        // set some vars
+        $iLimit = empty($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? 4 : getParam($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']);
+        if (!$iLimit)
+            $iLimit = 4;
+        $iStart = (int)bx_get('start');
+
+        // get connections array
+        bx_import('BxDolConnection');
+        $a = $oConnection->getConnectionsAsArray ($sConnectionsType, $iProfileId, $iProfileId2, $iMutual, (int)bx_get('start'), $iLimit + 1, BX_CONNECTIONS_ORDER_ADDED_DESC);
+        if (!$a)
+            return '';
+
+        // get paginate object
+        $oPaginate = new BxTemplPaginate(array(
+            'on_change_page' => "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}');",
+            'num' => count($a),
+            'per_page' => $iLimit,
+            'start' => $iStart,
+        ));
+
+        // remove last item from connection array, because we've got one more item for pagination calculations only
+        if (count($a) > $iLimit)
+            array_pop($a);
+
+        // get profiles HTML
+        $s = '';
+        foreach ($a as $iProfileId) {
+            if (!($o = BxDolProfile::getInstance($iProfileId))) {
+                continue;
+            }
+            $s .= $o->getUnit();
+        }
+
+        // return profiles + paginate
+        return $s . (!$iStart && $oPaginate->getNum() <= $iLimit ?  '' : $oPaginate->getSimplePaginate());
+    }
+
+	public function serviceBrowseByAcl ($mixedLevelId, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        return $this->_serviceBrowse (
+            'acl',
+            array(
+                'level' => $mixedLevelId,
+			),
+            $iDesignBox,
+            true
+        );
+    }
+
+    public function serviceEntityEditCover ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_COVER']);
+    }
+
+	/**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => 0,
+        	'id_thumb' => !empty($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']]) ? $aContentInfo[$CNF['FIELD_PICTURE']] : '',
+        	'title' => !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+        	'object_storage' => !empty($CNF['OBJECT_STORAGE']) ? $CNF['OBJECT_STORAGE'] : '',
+            'object_transcoder' => false,
+        	'object_vote' => '',
+        	'object_favorite' => !empty($CNF['OBJECT_FAVORITES']) ? $CNF['OBJECT_FAVORITES'] : '',
+        	'object_feature' => !empty($CNF['OBJECT_FEATURED']) ? $CNF['OBJECT_FEATURED'] : '',
+        	'object_report' => !empty($CNF['OBJECT_REPORTS']) ? $CNF['OBJECT_REPORTS'] : '',
+        	'uri_view_entry' => !empty($CNF['URI_VIEW_ENTRY']) ? $CNF['URI_VIEW_ENTRY'] : '',
+        	'social_sharing' => false
+        ));
+    }
+
+    public function serviceProfileMembership ($iContentId = 0)
+    {
+    	if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+		$aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+		return BxDolAcl::getInstance()->getProfileMembership($aContentInfo['profile_id']);
+    }
+    public function serviceProfileFriends ($iContentId = 0)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_friends', BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+        return $s;
+    }
+
+    /**
+     * For internal usage only.
+     */
+    public function serviceDeleteEntityService ($iContentId, $bDeleteWithContent = false)
+    {
+        return parent::serviceDeleteEntity ($iContentId, 'deleteDataService');
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+        $a = parent::serviceGetNotificationsData();
+
+        $sModule = $this->_aModule['name'];
+        
+        $a['handlers'][] = array('group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module');
+
+        $a['alerts'][] = array('unit' => $sModule, 'action' => 'timeline_post_common');
+
+        return $a;
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsTimelinePostCommon($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, '_bx_' . $this->_oConfig->getUri() . '_txt_ntfs_timeline_post_common');
+    }
+
+    public function serviceGetConnectionButtonsTitles($iProfileId, $sConnectionsObject = 'sys_profiles_friends')
+    {
+        if (!isLogged())
+            return array();
+
+        if (!($oConn = BxDolConnection::getObjectInstance($sConnectionsObject)))
+            return array();
+
+        $CNF = $this->_oConfig->CNF;
+
+        if ($oConn->isConnectedNotMutual(bx_get_logged_profile_id(), $iProfileId)) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_sent']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_cancel_request']),
+            );
+        } elseif ($oConn->isConnectedNotMutual($iProfileId, bx_get_logged_profile_id())) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_confirm']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_reject_request']),
+            );
+        } elseif ($oConn->isConnected($iProfileId, bx_get_logged_profile_id(), true)) {
+            return array(
+                'add' => '',
+                'remove' => _t($CNF['T']['menu_item_title_unfriend']),
+            );
+        } else {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend']),
+                'remove' => '',
+            );
+        }
+    }
+    
+    // ====== PERMISSION METHODS
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        $oAccount = BxDolAccount::getInstance();
+        if (!$oAccount || ($this->serviceActAsProfile() && $oAccount->isProfilesLimitReached()))
+            return _t('_sys_txt_access_denied');
+        return parent::checkAllowedAdd ($isPerformAction);
+    }   
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($oProfile && $oProfile->id() == $this->_iProfileId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewProfileImage ($aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedView($aDataEntry)) && BX_DOL_PG_FRIENDS == $aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']])
+            return CHECK_ACTION_RESULT_ALLOWED;
+        
+        return $sMsg;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedView($aDataEntry);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedPost ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account! not as profile as ususal) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * Check if user can change cover image
+     */
+    public function checkAllowedChangeCover ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account! not as profile as ususal) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL and owner (checked by account! not as profile as ususal)
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewMoreMenu (&$aDataEntry, $isPerformAction = false)
+    {
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_MORE']);
+        if (!$oMenu || !$oMenu->getCode())
+            return _t('_sys_txt_access_denied');
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+	/**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedView($aDataEntry)))
+            return $sMsg;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, true);
+    }
+
+    public function checkMyself($iContentId)
+    {
+		$iLogged = (int)bx_get_logged_profile_id();
+    	if(empty($iLogged))
+    		return false;
+
+    	$oProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->_oConfig->getName());
+    	if(!$oProfile)
+    		return false;
+
+		return $oProfile->id() == $iLogged;
+    }
+
+    // ====== PROTECTED METHODS
+
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        if (!$this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile || $oProfile->id() == $this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        $oConn = BxDolConnection::getObjectInstance($sObjConnection);
+        if ($isSwap)
+            $isConnected = $oConn->isConnected($oProfile->id(), $this->_iProfileId, $isMutual);
+        else
+            $isConnected = $oConn->isConnected($this->_iProfileId, $oProfile->id(), $isMutual);
+
+        if ($isInvertResult)
+            $isConnected = !$isConnected;
+
+        return $isConnected ? _t('_sys_txt_access_denied') : CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    protected function _buildRssParams($sMode, $aArgs)
+    {
+        $aParams = array ();
+        $sMode = bx_process_input($sMode);
+        switch ($sMode) {
+            case 'connections':
+                $aParams = array(
+                    'object' => isset($aArgs[0]) ? $aArgs[0] : '',
+                    'type' => isset($aArgs[1]) ? $aArgs[1] : '',
+                    'profile' => isset($aArgs[2]) ? (int)$aArgs[2] : 0,
+                    'mutual' => isset($aArgs[3]) ? (int)$aArgs[3] : 0,
+                    'profile2' => isset($aArgs[4]) ? (int)$aArgs[4] : 0,
+                );
+                break;
+        }
+
+        return $aParams;
+    }
+}
+
+/** @} */

+ 147 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextFormEntry.php

@@ -0,0 +1,147 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Create/Edit entry form
+ */
+class BxBaseModTextFormEntry extends BxBaseModGeneralFormEntry
+{
+    protected $_sGhostTemplate = 'form_ghost_template.html';
+
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($this->aInputs[$CNF['FIELD_TEXT']])) {
+            $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($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'] = '';
+        }
+
+        if (isset($this->aInputs[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            $this->aInputs[$CNF['FIELD_ALLOW_VIEW_TO']] = BxDolPrivacy::getGroupChooser($CNF['OBJECT_PRIVACY_VIEW']);
+
+        if (isset($this->aInputs['do_publish']) && !isset($this->aInputs['do_submit']))
+            $this->aParams['db']['submit_name'] = 'do_publish';
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (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));
+        }
+
+        return parent::initChecker($aValues, $aSpecificValues);
+    }
+
+    public function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb()) {
+            $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;
+        }
+
+        $iContentId = parent::insert ($aValsToAdd, $isIgnore);
+        if(!empty($iContentId))
+            $this->processFiles ($CNF['FIELD_PHOTO'], $iContentId, true);
+
+        return $iContentId;
+    }
+
+    function update ($iContentId, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb()) {
+            $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;
+        }
+
+        $iRet = parent::update ($iContentId, $aValsToAdd, $aTrackTextFieldsChanges);
+
+        $this->processFiles ($CNF['FIELD_PHOTO'], $iContentId, false);
+
+        return $iRet;
+    }
+
+    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);
+    }
+
+    protected function _associalFileWithContent($oStorage, $iFileId, $iProfileId, $iContentId, $sPictureField = '')
+    {
+        $oStorage->updateGhostsContentId ($iFileId, $iProfileId, $iContentId);
+    }
+
+    protected function _getPhotoGhostTmplVars($aContentInfo = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	return array (
+			'name' => $this->aInputs[$CNF['FIELD_PHOTO']]['name'],
+			'content_id' => $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'],
+			'editor_id' => $CNF['FIELD_TEXT_ID'],
+			'thumb_id' => isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) ? $aContentInfo[$CNF['FIELD_THUMB']] : 0,
+			'bx_if:set_thumb' => array (
+				'condition' => CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb(),
+				'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')
+				),
+			),
+		);
+    }
+}
+
+/** @} */

+ 149 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextGridAdministration.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    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModTextGridAdministration extends BxBaseModGeneralGridAdministration
+{
+	protected $_sFieldStatus;
+	protected $_sFilter1Name;
+	protected $_sFilter1Value;
+	protected $_aFilter1Values;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_sFieldStatus = $CNF['FIELD_STATUS_ADMIN'];
+
+        $this->_sFilter1Name = 'filter1';
+        $this->_aFilter1Values = array(
+        	'active' => $CNF['T']['filter_item_active'],
+            'hidden' => $CNF['T']['filter_item_hidden'],
+		);
+        
+    	$sFilter1 = bx_get($this->_sFilter1Name);
+        if(!empty($sFilter1)) {
+            $this->_sFilter1Value = bx_process_input($sFilter1);
+            $this->_aQueryAppend['filter1'] = $this->_sFilter1Value;
+        }
+    }
+
+    protected function _switcherChecked2State($isChecked)
+    {
+        return $isChecked ? 'active' : 'hidden';
+    }
+
+    protected function _switcherState2Checked($mixedState)
+    {
+        return 'active' == $mixedState ? true : false;
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if(strpos($sFilter, $this->_sParamsDivider) !== false)
+            list($this->_sFilter1Value, $sFilter) = explode($this->_sParamsDivider, $sFilter);
+
+    	if(!empty($this->_sFilter1Value))
+        	$this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `" . $this->_sFieldStatus . "`=?", $this->_sFilter1Value);
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    //--- Layout methods ---//
+	protected function _getFilterControls()
+    {
+        parent::_getFilterControls();
+
+        return  $this->_getFilterSelectOne($this->_sFilter1Name, $this->_sFilter1Value, $this->_aFilter1Values) . $this->_getSearchInput();
+    }
+
+	protected function _getCellTitle($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault($this->_getEntryLink($mixedValue, $aRow), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAdded($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAuthor($mixedValue, $sKey, $aField, $aRow)
+    {
+    	$oProfile = $this->_getProfileObject($aRow['author']);
+    	$sProfile = $oProfile->getDisplayName();
+
+		$oAcl = BxDolAcl::getInstance();
+
+    	$sAccountEmail = '';
+    	$sManageAccountUrl = '';
+    	if($oProfile && $oProfile instanceof BxDolProfile && $oAcl->isMemberLevelInSet(128)) {
+    		$sAccountEmail = $oProfile->getAccountObject()->getEmail();
+	    	$sManageAccountUrl = $this->_getManageAccountUrl($sAccountEmail);
+    	}
+
+        $mixedValue = $this->_oTemplate->parseHtmlByName('author_link.html', array(
+            'href' => $oProfile->getUrl(),
+            'title' => $sProfile,
+            'content' => $sProfile,
+        	'bx_if:show_account' => array(
+        		'condition' => !empty($sManageAccountUrl), 
+        		'content' => array(
+        			'href' => $sManageAccountUrl,
+		        	'title' => _t($this->_oModule->_oConfig->CNF['T']['grid_txt_account_manager']),
+		        	'content' => $sAccountEmail
+        		)
+        	)
+        ));
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionEdit($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 '';
+
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_EDIT_ENTRY'] . '&id=' . $aRow[$CNF['FIELD_ID']]);
+
+    	$a['attr'] = array_merge($a['attr'], array(
+    		"onclick" => "window.open('" . $sUrl . "','_self');"
+    	));
+
+    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getProfileObject($iId)
+    {
+        $oProfile = BxDolProfile::getInstance($iId);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+    protected function _getEntryLink($mixedValue, $aRow)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aRow[$CNF['FIELD_ID']]);
+
+        return $this->_oTemplate->parseHtmlByName('title_link.html', array(
+            'href' => $sUrl,
+            'title' => bx_html_attribute($mixedValue),
+            'content' => $mixedValue
+        ));
+    }
+}
+
+/** @} */

+ 35 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextMenu.php

@@ -0,0 +1,35 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * General class for module menu.
+ */
+class BxBaseModTextMenu extends BxBaseModGeneralMenuView
+{
+    protected $_iContentId;
+    protected $_aContentInfo;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+    }
+
+    public function setContentId($iContentId)
+    {
+        $this->_iContentId = (int)$iContentId;
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_iContentId);
+        if($this->_aContentInfo)
+            $this->addMarkers(array('content_id' => (int)$this->_iContentId));
+    }
+}
+
+/** @} */

+ 25 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextMenuView.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    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * View entry menu
+ */
+class BxBaseModTextMenuView extends BxBaseModTextMenu
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+ 
+        $this->setContentId(bx_process_input(bx_get('id'), BX_DATA_INT));
+    }
+}
+
+/** @} */

+ 205 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextModule.php

@@ -0,0 +1,205 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Base module class for text based modules
+ */
+class BxBaseModTextModule extends BxBaseModGeneralModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    // ====== SERVICE METHODS
+	public function serviceGetMenuAddonManageTools()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'status' => 'hidden'
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+	public function serviceGetMenuAddonManageToolsProfileStats()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'author' => bx_get_logged_profile_id()
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+    /**
+     * Display public entries
+     * @return HTML string
+     */
+    public function serviceBrowsePublic ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {   
+        return $this->_serviceBrowse ('public', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * Display popular entries
+     * @return HTML string
+     */
+    public function serviceBrowsePopular ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('popular', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * Display recently updated entries
+     * @return HTML string
+     */
+    public function serviceBrowseUpdated ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('updated', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * Display entries of the author
+     * @return HTML string
+     */
+    public function serviceBrowseAuthor ($iProfileId = 0, $aParams = array())
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+        return $this->_serviceBrowse ('author', array_merge(array('author' => $iProfileId), $aParams), BX_DB_PADDING_DEF, true);
+    }
+
+    /**
+     * Entry comments
+     */
+    public function serviceEntityComments ($iContentId = 0)
+    {
+        return $this->_entityComments($this->_oConfig->CNF['OBJECT_COMMENTS'], $iContentId);
+    }
+
+    /**
+     * Entry author block
+     */
+    public function serviceEntityAuthor ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryAuthor', $iContentId);
+    }
+
+    /**
+     * Entry attachments block
+     */
+    public function serviceEntityAttachments ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryAttachments', $iContentId);
+    }
+
+    public function serviceEntityBreadcrumb ($iContentId = 0)
+    {
+    	if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+		return $this->_oTemplate->entryBreadcrumb($aContentInfo);
+    }
+
+    /**
+     * My entries actions block
+     */
+    public function serviceMyEntriesActions ($iProfileId = 0)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId || !($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return false;
+
+        if ($iProfileId != $this->_iProfileId)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_MY_ENTRIES']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+    /**
+     * Delete all content by profile 
+     * @param $iProfileId profile id 
+     * @return number of deleted items
+     */
+    public function serviceDeleteEntitiesByAuthor ($iProfileId)
+    {
+        $a = $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+        if (!$a)
+            return 0;
+
+        $iCount = 0;
+        foreach ($a as $aContentInfo)
+            $iCount += ('' == $this->serviceDeleteEntity($aContentInfo[$this->_oConfig->CNF['FIELD_ID']]) ? 1 : 0);
+
+        return $iCount;
+    }
+
+    // ====== PERMISSION METHODS
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedSetThumb ()
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'set thumb', $this->getName(), false);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    // ====== PROTECTED METHODS
+
+    protected function _buildRssParams($sMode, $aArgs)
+    {
+        $aParams = array ();
+        $sMode = bx_process_input($sMode);
+        switch ($sMode) {
+            case 'author':
+                $aParams = array('author' => isset($aArgs[0]) ? (int)$aArgs[0] : '');
+                break;
+        }
+
+        return $aParams;
+    }
+
+    protected function _entityComments ($sObject, $iId = 0)
+    {
+        if (!$iId)
+            $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iId)
+            return false;
+
+        $oCmts = BxDolCmts::getObjectInstance($sObject, $iId);
+        if (!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        return $oCmts->getCommentsBlock(0, 0, false);
+    }
+}
+
+/** @} */

+ 67 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextPageEntry.php

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

+ 303 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/classes/BxBaseModTextTemplate.php

@@ -0,0 +1,303 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Module representation.
+ */
+class BxBaseModTextTemplate extends BxBaseModGeneralTemplate
+{
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+    function unit ($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html', $aParams = array())
+    {
+    	$sResult = $this->checkPrivacy ($aData, $isCheckPrivateContent, $this->getModule(), $sTemplateName);
+    	if($sResult)
+            return $sResult;
+
+		return $this->parseHtmlByName($sTemplateName, $this->getUnit($aData, $aParams));
+    }
+
+    function entryAuthor ($aData, $iProfileId = false, $sFuncAuthorDesc = 'getAuthorDesc', $sTemplateName = 'author.html', $sFuncAuthorAddon = 'getAuthorAddon')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        if (!$iProfileId)
+            $iProfileId = $aData[$CNF['FIELD_AUTHOR']];
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        if (!$oProfile)
+            return '';
+
+        $sAddon = $sFuncAuthorAddon ? $this->$sFuncAuthorAddon($aData, $oProfile) : '';
+
+        $aVars = array (
+            'author_url' => $oProfile->getUrl(),
+            'author_thumb_url' => $oProfile->getThumb(),
+            'author_title' => $oProfile->getDisplayName(),
+            'author_desc' => $sFuncAuthorDesc ? $this->$sFuncAuthorDesc($aData) : '',
+            'bx_if:addon' => array (
+                'condition' => (bool)$sAddon,
+                'content' => array (
+                    'content' => $sAddon,
+                ),
+            ),
+        );
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    function getAuthorDesc ($aData)
+    {
+        return '';
+    }
+
+    function getAuthorAddon ($aData, $oProfile)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        $sUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $oProfile->id();
+        $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl);
+        return _t($CNF['T']['txt_all_entries_by'], $sUrl, $oProfile->getDisplayName(), $this->getModule()->_oDb->getEntriesNumByAuthor($oProfile->id()));
+    }
+
+    public function entryBreadcrumb($aContentInfo, $aTmplVarsItems = array())
+    {
+    	$CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+    	return $this->parseHtmlByName('breadcrumb.html', array(
+    		'url_home' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($CNF['URL_HOME']),
+    		'icon_home' => $CNF['ICON'],
+    		'bx_repeat:items' => $aTmplVarsItems
+    	));
+    }
+
+    function entryAttachments ($aData)
+    {
+        if (!($a = $this->getAttachments($this->getModule()->_oConfig->CNF['OBJECT_STORAGE'], $aData)))
+            return '';
+    	return $this->parseHtmlByName('attachments.html', array(
+            'bx_repeat:attachments' => $a,
+        ));
+    }
+
+	function entryAttachmentsByStorage ($sStorage, $aData)
+    {
+        if (!($a = $this->getAttachments($sStorage, $aData)))
+            return '';
+    	return $this->parseHtmlByName('attachments.html', array(
+            'bx_repeat:attachments' => $a,
+        ));
+    }
+
+    protected function checkPrivacy ($aData, $isCheckPrivateContent, $oModule, $sTemplateName = '')
+    {
+        if ($isCheckPrivateContent && CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $oModule->checkAllowedView($aData))) {
+            $aVars = array (
+                'summary' => $sMsg,
+            );
+            return $this->parseHtmlByName($sTemplateName ? str_replace('.html', '_private.html', $sTemplateName) : 'unit_private.html', $aVars);
+        }
+
+        return '';
+    }
+
+    protected function getUnitThumbAndGallery ($aData)
+    {
+        $CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+        $sPhotoThumb = '';
+        $sPhotoGallery = '';
+        if(!empty($CNF['FIELD_THUMB']) && $aData[$CNF['FIELD_THUMB']]) {
+
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW']);
+            if ($oImagesTranscoder)
+                $sPhotoThumb = $oImagesTranscoder->getFileUrl($aData[$CNF['FIELD_THUMB']]);
+
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            if ($oImagesTranscoder)
+                $sPhotoGallery = $oImagesTranscoder->getFileUrl($aData[$CNF['FIELD_THUMB']]);
+            else
+                $sPhotoGallery = $sPhotoThumb;
+        }
+
+        return array($sPhotoThumb, $sPhotoGallery);
+    }
+
+	protected function getUnit ($aData, $aParams = array())
+    {
+        $CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+        // get thumb url
+        list($sPhotoThumb, $sPhotoGallery) = $this->getUnitThumbAndGallery($aData);
+
+        // get entry url
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $oProfile = BxDolProfile::getInstance($aData[$CNF['FIELD_AUTHOR']]);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        $sTitle = $this->getTitle($aData);
+        $sText = $this->getText($aData);
+        $sSummary = $this->getSummary($aData, $sTitle, $sText, $sUrl);
+        $sSummaryPlain = isset($CNF['PARAM_CHARS_SUMMARY_PLAIN']) && $CNF['PARAM_CHARS_SUMMARY_PLAIN'] ? BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($sSummary), (int)getParam($CNF['PARAM_CHARS_SUMMARY_PLAIN'])) : '';
+
+        if(!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+    
+            // keywords
+            if ($oMetatags->keywordsIsEnabled())
+                $sText = $oMetatags->keywordsParse($aData[$CNF['FIELD_ID']], $sText);
+        }
+
+        $sTitle = bx_process_output($sTitle);
+
+        // generate html
+        return array (
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            'title' => $sTitle,
+            'title_attr' => bx_html_attribute($sTitle),
+            'summary' => $sSummary,
+            'text' => $sText,
+            'author' => $oProfile->getDisplayName(),
+            'author_url' => $oProfile->getUrl(),
+            'entry_posting_date' => bx_time_js($aData[$CNF['FIELD_ADDED']], BX_FORMAT_DATE),
+            'module_name' => _t($CNF['T']['txt_sample_single']),
+            'ts' => $aData[$CNF['FIELD_ADDED']],
+            'bx_if:thumb' => array (
+                'condition' => $sPhotoThumb,
+                'content' => array (
+                    'title' => $sTitle,
+                    'summary_attr' => bx_html_attribute($sSummaryPlain),
+                    'content_url' => $sUrl,
+                    'thumb_url' => $sPhotoThumb ? $sPhotoThumb : '',
+                    'gallery_url' => $sPhotoGallery ? $sPhotoGallery : '',
+                    'strecher' => str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', 40),
+                ),
+            ),
+            'bx_if:no_thumb' => array (
+                'condition' => !$sPhotoThumb,
+                'content' => array (
+                    'content_url' => $sUrl,
+                    'summary_plain' => $sSummaryPlain,
+                    'strecher' => mb_strlen($sSummaryPlain) > 240 ? '' : str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', round((240 - mb_strlen($sSummaryPlain)) / 6)),
+                ),
+            ),
+        );
+    }
+
+    protected function getTitle($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        return $aData[$CNF['FIELD_TITLE']];
+    }
+
+    protected function getText($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        return $aData[$CNF['FIELD_TEXT']];
+    }
+
+    protected function getSummary($aData, $sTitle = '', $sText = '', $sUrl = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        if(empty($CNF['PARAM_CHARS_SUMMARY']))
+            return '';
+
+        // get summary
+        $sLinkMore = ' <a title="' . bx_html_attribute(_t('_sys_read_more', $sTitle)) . '" href="' . $sUrl . '"><i class="sys-icon ellipsis-h"></i></a>';
+        return  strmaxtextlen($sText, (int)getParam($CNF['PARAM_CHARS_SUMMARY']), $sLinkMore);
+    }
+
+	protected function getAttachments ($sStorage, $aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $oStorage = BxDolStorage::getObjectInstance($sStorage);
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW']);
+        $aTranscodersVideo = false;
+
+        if ($CNF['OBJECT_VIDEOS_TRANSCODERS'])
+            $aTranscodersVideo = array (
+                'poster' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['poster']),
+                'mp4' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']),
+                'webm' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['webm']),
+            );
+
+        $aGhostFiles = $oStorage->getGhosts ($aData[$CNF['FIELD_AUTHOR']], $aData[$CNF['FIELD_ID']]);
+        if (!$aGhostFiles)
+            return false;
+
+		if(isset($aData[$CNF['FIELD_THUMB']]))
+	        foreach ($aGhostFiles as $k => $a) {
+	            // don't show thumbnail in attachments
+	            if ($a['id'] == $aData[$CNF['FIELD_THUMB']])
+	                unset($aGhostFiles[$k]);
+	        }
+
+        if(!$aGhostFiles)
+            return false;
+
+        foreach ($aGhostFiles as $k => $a) {
+
+            $isImage = $oTranscoder && (0 === strncmp('image/', $a['mime_type'], 6)); // preview for images, transcoder object for preview must be defined
+            $isVideo = $aTranscodersVideo && (0 === strncmp('video/', $a['mime_type'], 6)); // preview for videos, transcoder object for video must be defined
+            $sUrlOriginal = $oStorage->getFileUrlById($a['id']);
+            $sImgPopupId = 'bx-messages-atachment-popup-' . $a['id'];
+
+            // images are displayed with preview and popup upon clicking
+            $aGhostFiles[$k]['bx_if:image'] = array (
+                'condition' => $isImage,
+                'content' => array (
+                    'url_original' => $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'popup_id' => $sImgPopupId,
+                    'url_preview' => $isImage ? $oTranscoder->getFileUrl($a['id']) : '',
+                    'popup' =>  BxTemplFunctions::getInstance()->transBox($sImgPopupId, '<img src="' . $sUrlOriginal . '" />', true, true),
+                ),
+            );
+
+            // videos are displayed inline
+            $aGhostFiles[$k]['bx_if:video'] = array (
+                'condition' => $isVideo,
+                'content' => array (
+                    'video' => $isVideo && $aTranscodersVideo ? BxTemplFunctions::getInstance()->videoPlayer(
+                        $aTranscodersVideo['poster']->getFileUrl($a['id']), 
+                        $aTranscodersVideo['mp4']->getFileUrl($a['id']), 
+                        $aTranscodersVideo['webm']->getFileUrl($a['id']),
+                        false, ''
+                    ) : '',
+                ),
+            );
+
+            // non-images are displayed as text links to original file
+            $aGhostFiles[$k]['bx_if:not_image'] = array (
+                'condition' => !$isImage && !$isVideo,
+                'content' => array (
+                    'url_original' => $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'file_name' => bx_process_output($a['file_name']),
+                ),
+            );
+        }
+
+        return $aGhostFiles;
+    }
+}
+
+/** @} */

+ 28 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/attachments.html

@@ -0,0 +1,28 @@
+<div class="bx-base-text-attachments bx-clearfix">
+
+    <bx_repeat:attachments>
+        <div class="bx-base-text-attachment bx-def-box bx-def-color-bg-box bx-def-gal-img-size" style="float:left;">
+            <bx_if:image>
+                <a href="javascript:void(0);" title="__attr_file_name__" onclick="$('#__popup_id__').dolPopupImage('__url_original__', $(this).parent())">
+                    <img class="bx-def-gal-img-size" src="__url_preview__" />
+                </a>
+                __popup__
+            </bx_if:image>
+            <bx_if:video>
+                __video__
+            </bx_if:video>
+            <bx_if:not_image>
+                <a href="__url_original__" title="__attr_file_name__">
+                    <div class="bx-def-round-corners bx-def-padding-sec">__file_name__</div>
+                </a>
+            </bx_if:not_image>
+        </div>
+    </bx_repeat:attachments>
+
+    <script>
+        $(document).ready(function() {
+            bx_center_content('.bx-base-text-attachments', '.bx-base-text-attachment', true);
+        });
+    </script>
+
+</div>

+ 1 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/author_link.html

@@ -0,0 +1 @@
+<a class="bx-base-text-author" href="__href__" title="__title__">__content__</a><bx_if:show_account><span class="bx-def-font-small bx-def-font-grayed"><br />(<a href="__href__" title="__title__" target="_blank" class="bx-def-font-grayed">__content__</a>)</span></bx_if:show_account>

+ 225 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/css/main.css

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

+ 8 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/unit_full_private.html

@@ -0,0 +1,8 @@
+
+<div class="bx-base-text-unit bx-base-text-unit-ext bx-def-margin-bottom">
+    <h1 class="bx-def-padding-sec-top"><a href="__content_url__"><bx_text:_sys_private /></a></h1>
+    <div class="bx-base-text-unit-info bx-def-margin-sec-top">
+        <span class="bx-def-font-grayed">__summary__</span>
+    </div>
+</div>
+

+ 10 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/modules/base/text/template/unit_gallery_private.html

@@ -0,0 +1,10 @@
+<div class="bx-base-text-unit bx-base-text-unit-gallery bx-def-gal-unit-size bx-def-padding-sec bx-def-margin-sec-bottom">
+	<div class="bx-base-text-unit-cnt bx-def-box bx-def-round-corners bx-def-color-bg-box bx-def-box-sizing">
+		<div class="bx-base-text-unit-text bx-def-padding-leftright bx-def-padding-sec-topbottom bx-def-font-align-center bx-def-box-sizing">
+			<h2><bx_text:_sys_private /></h2>
+		    <div class="bx-base-text-unit-info">
+		        <span class="bx-def-font-grayed">__summary__</span>
+		    </div>
+		</div>
+	</div>
+</div>

+ 393 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/AuthorizeController.php

@@ -0,0 +1,393 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\ClientInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController implements AuthorizeControllerInterface
+{
+    private $scope;
+    private $state;
+    private $client_id;
+    private $redirect_uri;
+    private $response_type;
+
+    protected $clientStorage;
+    protected $responseTypes;
+    protected $config;
+    protected $scopeUtil;
+
+    /**
+     * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information
+     * @param array                          $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects.  Valid array
+     *                                                      keys are "code" and "token"
+     * @param array                          $config        OPTIONAL Configuration options for the server
+     *                                                      <code>
+     *                                                      $config = array(
+     *                                                      'allow_implicit' => false,            // if the controller should allow the "implicit" grant type
+     *                                                      'enforce_state'  => true              // if the controller should require the "state" parameter
+     *                                                      'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter
+     *                                                      'redirect_status_code' => 302,        // HTTP status code to use for redirect responses
+     *                                                      );
+     *                                                      </code>
+     * @param OAuth2\ScopeInterface          $scopeUtil     OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
+     */
+    public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        $this->clientStorage = $clientStorage;
+        $this->responseTypes = $responseTypes;
+        $this->config = array_merge(array(
+            'allow_implicit' => false,
+            'enforce_state'  => true,
+            'require_exact_redirect_uri' => true,
+            'redirect_status_code' => 302,
+        ), $config);
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+    {
+        if (!is_bool($is_authorized)) {
+            throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean.  This method must know if the user has granted access to the client.');
+        }
+
+        // We repeat this, because we need to re-validate. The request could be POSTed
+        // by a 3rd-party (because we are not internally enforcing NONCEs, etc)
+        if (!$this->validateAuthorizeRequest($request, $response)) {
+            return;
+        }
+
+        // If no redirect_uri is passed in the request, use client's registered one
+        if (empty($this->redirect_uri)) {
+            $clientData              = $this->clientStorage->getClientDetails($this->client_id);
+            $registered_redirect_uri = $clientData['redirect_uri'];
+        }
+
+        // the user declined access to the client's application
+        if ($is_authorized === false) {
+            $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri;
+            $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id);
+
+            return;
+        }
+
+        // build the parameters to set in the redirect URI
+        if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) {
+            return;
+        }
+
+        $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id);
+
+        list($redirect_uri, $uri_params) = $authResult;
+
+        if (empty($redirect_uri) && !empty($registered_redirect_uri)) {
+            $redirect_uri = $registered_redirect_uri;
+        }
+
+        $uri = $this->buildUri($redirect_uri, $uri_params);
+
+        // return redirect response
+        $response->setRedirect($this->config['redirect_status_code'], $uri);
+    }
+
+    protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+    {
+        $error = 'access_denied';
+        $error_message = 'The user denied access to your application';
+        $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message);
+    }
+
+    /*
+     * We have made this protected so this class can be extended to add/modify
+     * these parameters
+     */
+    protected function buildAuthorizeParameters($request, $response, $user_id)
+    {
+        // @TODO: we should be explicit with this in the future
+        $params = array(
+            'scope'         => $this->scope,
+            'state'         => $this->state,
+            'client_id'     => $this->client_id,
+            'redirect_uri'  => $this->redirect_uri,
+            'response_type' => $this->response_type,
+        );
+
+        return $params;
+    }
+
+    /**
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI)
+        if (!$client_id = $request->query('client_id', $request->request('client_id'))) {
+            // We don't have a good URI to use
+            $response->setError(400, 'invalid_client', "No client id supplied");
+
+            return false;
+        }
+
+        // Get client details
+        if (!$clientData = $this->clientStorage->getClientDetails($client_id)) {
+            $response->setError(400, 'invalid_client', 'The client id supplied is invalid');
+
+            return false;
+        }
+
+        $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : '';
+
+        // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI.
+        // @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+        // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1
+        // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1
+        if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) {
+            // validate there is no fragment supplied
+            $parts = parse_url($supplied_redirect_uri);
+            if (isset($parts['fragment']) && $parts['fragment']) {
+                $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment');
+
+                return false;
+            }
+
+            // validate against the registered redirect uri(s) if available
+            if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) {
+                $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2');
+
+                return false;
+            }
+            $redirect_uri = $supplied_redirect_uri;
+        } else {
+            // use the registered redirect_uri if none has been supplied, if possible
+            if (!$registered_redirect_uri) {
+                $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored');
+
+                return false;
+            }
+
+            if (count(explode(' ', $registered_redirect_uri)) > 1) {
+                $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3');
+
+                return false;
+            }
+            $redirect_uri = $registered_redirect_uri;
+        }
+
+        // Select the redirect URI
+        $response_type = $request->query('response_type', $request->request('response_type'));
+
+        // for multiple-valued response types - make them alphabetical
+        if (false !== strpos($response_type, ' ')) {
+            $types = explode(' ', $response_type);
+            sort($types);
+            $response_type = ltrim(implode(' ', $types));
+        }
+
+        $state = $request->query('state', $request->request('state'));
+
+        // type and client_id are required
+        if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) {
+            $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null);
+
+            return false;
+        }
+
+        if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+            if (!isset($this->responseTypes['code'])) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null);
+
+                return false;
+            }
+            if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+                return false;
+            }
+            if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) {
+                $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied');
+
+                return false;
+            }
+        } else {
+            if (!$this->config['allow_implicit']) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null);
+
+                return false;
+            }
+            if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+                return false;
+            }
+        }
+
+        // validate requested scope if it exists
+        $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+
+        if ($requestedScope) {
+            // restrict scope by client specific scope if applicable,
+            // otherwise verify the scope exists
+            $clientScope = $this->clientStorage->getClientScope($client_id);
+            if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope))
+                || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null);
+
+                return false;
+            }
+        } else {
+            // use a globally-defined default scope
+            $defaultScope = $this->scopeUtil->getDefaultScope($client_id);
+
+            if (false === $defaultScope) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null);
+
+                return false;
+            }
+
+            $requestedScope = $defaultScope;
+        }
+
+        // Validate state parameter exists (if configured to enforce this)
+        if ($this->config['enforce_state'] && !$state) {
+            $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required');
+
+            return false;
+        }
+
+        // save the input data and return true
+        $this->scope         = $requestedScope;
+        $this->state         = $state;
+        $this->client_id     = $client_id;
+        // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3)
+        $this->redirect_uri  = $supplied_redirect_uri;
+        $this->response_type = $response_type;
+
+        return true;
+    }
+
+    /**
+     * Build the absolute URI based on supplied URI and parameters.
+     *
+     * @param $uri    An absolute URI.
+     * @param $params Parameters to be append as GET.
+     *
+     * @return
+     * An absolute URI with supplied parameters.
+     *
+     * @ingroup oauth2_section_4
+     */
+    private function buildUri($uri, $params)
+    {
+        $parse_url = parse_url($uri);
+
+        // Add our params to the parsed uri
+        foreach ($params as $k => $v) {
+            if (isset($parse_url[$k])) {
+                $parse_url[$k] .= "&" . http_build_query($v, '', '&');
+            } else {
+                $parse_url[$k] = http_build_query($v, '', '&');
+            }
+        }
+
+        // Put humpty dumpty back together
+        return
+            ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
+            . ((isset($parse_url["user"])) ? $parse_url["user"]
+            . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "")
+            . ((isset($parse_url["host"])) ? $parse_url["host"] : "")
+            . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
+            . ((isset($parse_url["path"])) ? $parse_url["path"] : "")
+            . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "")
+            . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "")
+        ;
+    }
+
+    protected function getValidResponseTypes()
+    {
+        return array(
+            self::RESPONSE_TYPE_ACCESS_TOKEN,
+            self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+        );
+    }
+
+    /**
+     * Internal method for validating redirect URI supplied
+     *
+     * @param string $inputUri            The submitted URI to be validated
+     * @param string $registeredUriString The allowed URI(s) to validate against.  Can be a space-delimited string of URIs to
+     *                                    allow for multiple URIs
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+     */
+    protected function validateRedirectUri($inputUri, $registeredUriString)
+    {
+        if (!$inputUri || !$registeredUriString) {
+            return false; // if either one is missing, assume INVALID
+        }
+
+        $registered_uris = preg_split('/\s+/', $registeredUriString);
+        foreach ($registered_uris as $registered_uri) {
+            if ($this->config['require_exact_redirect_uri']) {
+                // the input uri is validated against the registered uri using exact match
+                if (strcmp($inputUri, $registered_uri) === 0) {
+                    return true;
+                }
+            } else {
+                $registered_uri_length = strlen($registered_uri);
+                if ($registered_uri_length === 0) {
+                    return false;
+                }
+
+                // the input uri is validated against the registered uri using case-insensitive match of the initial string
+                // i.e. additional query parameters may be applied
+                if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convenience methods to access the parameters derived from the validated request
+     */
+
+    public function getScope()
+    {
+        return $this->scope;
+    }
+
+    public function getState()
+    {
+        return $this->state;
+    }
+
+    public function getClientId()
+    {
+        return $this->client_id;
+    }
+
+    public function getRedirectUri()
+    {
+        return $this->redirect_uri;
+    }
+
+    public function getResponseType()
+    {
+        return $this->response_type;
+    }
+}

+ 111 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/ResourceController.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\ResourceControllerInterface
+ */
+class ResourceController implements ResourceControllerInterface
+{
+    private $token;
+
+    protected $tokenType;
+    protected $tokenStorage;
+    protected $config;
+    protected $scopeUtil;
+
+    public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        $this->tokenType = $tokenType;
+        $this->tokenStorage = $tokenStorage;
+
+        $this->config = array_merge(array(
+            'www_realm' => 'Service',
+        ), $config);
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null)
+    {
+        $token = $this->getAccessTokenData($request, $response);
+
+        // Check if we have token data
+        if (is_null($token)) {
+            return false;
+        }
+
+        /**
+         * Check scope, if provided
+         * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403
+         * @see http://tools.ietf.org/html/rfc6750#section-3.1
+         */
+        if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) {
+            $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token');
+            $response->addHttpHeaders(array(
+                'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"',
+                    $this->tokenType->getTokenType(),
+                    $this->config['www_realm'],
+                    $scope,
+                    $response->getParameter('error'),
+                    $response->getParameter('error_description')
+                )
+            ));
+
+            return false;
+        }
+
+        // allow retrieval of the token
+        $this->token = $token;
+
+        return (bool) $token;
+    }
+
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response)
+    {
+        // Get the token parameter
+        if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) {
+            // Get the stored token data (from the implementing subclass)
+            // Check we have a well formed token
+            // Check token expiration (expires is a mandatory paramter)
+            if (!$token = $this->tokenStorage->getAccessToken($token_param)) {
+                $response->setError(401, 'invalid_token', 'The access token provided is invalid');
+            } elseif (!isset($token["expires"]) || !isset($token["client_id"])) {
+                $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")');
+            } elseif (time() > $token["expires"]) {
+                $response->setError(401, 'invalid_token', 'The access token provided has expired');
+            } else {
+                return $token;
+            }
+        }
+
+        $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']);
+
+        if ($error = $response->getParameter('error')) {
+            $authHeader = sprintf('%s, error="%s"', $authHeader, $error);
+            if ($error_description = $response->getParameter('error_description')) {
+                $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description);
+            }
+        }
+
+        $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader));
+
+        return null;
+    }
+
+    // convenience method to allow retrieval of the token
+    public function getToken()
+    {
+        return $this->token;
+    }
+}

+ 295 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Controller/TokenController.php

@@ -0,0 +1,295 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\Scope;
+use OAuth2\Storage\ClientInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see \OAuth2\Controller\TokenControllerInterface
+ */
+class TokenController implements TokenControllerInterface
+{
+    /**
+     * @var AccessTokenInterface
+     */
+    protected $accessToken;
+
+    /**
+     * @var array
+     */
+    protected $grantTypes;
+
+    /**
+     * @var ClientAssertionTypeInterface
+     */
+    protected $clientAssertionType;
+
+    /**
+     * @var Scope|ScopeInterface
+     */
+    protected $scopeUtil;
+
+    /**
+     * @var ClientInterface
+     */
+    protected $clientStorage;
+
+    public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null)
+    {
+        if (is_null($clientAssertionType)) {
+            foreach ($grantTypes as $grantType) {
+                if (!$grantType instanceof ClientAssertionTypeInterface) {
+                    throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface');
+                }
+            }
+        }
+        $this->clientAssertionType = $clientAssertionType;
+        $this->accessToken = $accessToken;
+        $this->clientStorage = $clientStorage;
+        foreach ($grantTypes as $grantType) {
+            $this->addGrantType($grantType);
+        }
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function handleTokenRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if ($token = $this->grantAccessToken($request, $response)) {
+            // @see http://tools.ietf.org/html/rfc6749#section-5.1
+            // server MUST disable caching in headers when tokens are involved
+            $response->setStatusCode(200);
+            $response->addParameters($token);
+            $response->addHttpHeaders(array(
+                'Cache-Control' => 'no-store',
+                'Pragma' => 'no-cache',
+                'Content-Type' => 'application/json'
+            ));
+        }
+    }
+
+    /**
+     * Grant or deny a requested access token.
+     * This would be called from the "/token" endpoint as defined in the spec.
+     * You can call your endpoint whatever you want.
+     *
+     * @param RequestInterface $request Request object to grant access token
+     * @param ResponseInterface $response
+     *
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @see http://tools.ietf.org/html/rfc6749#section-10.6
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response)
+    {
+        if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+            $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2');
+            $response->addHttpHeaders(array('Allow' => 'POST'));
+
+            return null;
+        }
+
+        /**
+         * Determine grant type from request
+         * and validate the request for that grant type
+         */
+        if (!$grantTypeIdentifier = $request->request('grant_type')) {
+            $response->setError(400, 'invalid_request', 'The grant type was not specified in the request');
+
+            return null;
+        }
+
+        if (!isset($this->grantTypes[$grantTypeIdentifier])) {
+            /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */
+            $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier));
+
+            return null;
+        }
+
+        $grantType = $this->grantTypes[$grantTypeIdentifier];
+
+        /**
+         * Retrieve the client information from the request
+         * ClientAssertionTypes allow for grant types which also assert the client data
+         * in which case ClientAssertion is handled in the validateRequest method
+         *
+         * @see OAuth2\GrantType\JWTBearer
+         * @see OAuth2\GrantType\ClientCredentials
+         */
+        if (!$grantType instanceof ClientAssertionTypeInterface) {
+            if (!$this->clientAssertionType->validateRequest($request, $response)) {
+                return null;
+            }
+            $clientId = $this->clientAssertionType->getClientId();
+        }
+
+        /**
+         * Retrieve the grant type information from the request
+         * The GrantTypeInterface object handles all validation
+         * If the object is an instance of ClientAssertionTypeInterface,
+         * That logic is handled here as well
+         */
+        if (!$grantType->validateRequest($request, $response)) {
+            return null;
+        }
+
+        if ($grantType instanceof ClientAssertionTypeInterface) {
+            $clientId = $grantType->getClientId();
+        } else {
+            // validate the Client ID (if applicable)
+            if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) {
+                $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier));
+
+                return null;
+            }
+        }
+
+        /**
+         * Validate the client can use the requested grant type
+         */
+        if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) {
+            $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id');
+
+            return false;
+        }
+
+        /**
+         * Validate the scope of the token
+         *
+         * requestedScope - the scope specified in the token request
+         * availableScope - the scope associated with the grant type
+         *  ex: in the case of the "Authorization Code" grant type,
+         *  the scope is specified in the authorize request
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-3.3
+         */
+
+        $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+        $availableScope = $grantType->getScope();
+
+        if ($requestedScope) {
+            // validate the requested scope
+            if ($availableScope) {
+                if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) {
+                    $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request');
+
+                    return null;
+                }
+            } else {
+                // validate the client has access to this scope
+                if ($clientScope = $this->clientStorage->getClientScope($clientId)) {
+                    if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) {
+                        $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client');
+
+                        return false;
+                    }
+                } elseif (!$this->scopeUtil->scopeExists($requestedScope)) {
+                    $response->setError(400, 'invalid_scope', 'An unsupported scope was requested');
+
+                    return null;
+                }
+            }
+        } elseif ($availableScope) {
+            // use the scope associated with this grant type
+            $requestedScope = $availableScope;
+        } else {
+            // use a globally-defined default scope
+            $defaultScope = $this->scopeUtil->getDefaultScope($clientId);
+
+            // "false" means default scopes are not allowed
+            if (false === $defaultScope) {
+                $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter');
+
+                return null;
+            }
+
+            $requestedScope = $defaultScope;
+        }
+
+        return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope);
+    }
+
+    /**
+     * addGrantType
+     *
+     * @param GrantTypeInterface $grantType the grant type to add for the specified identifier
+     * @param string $identifier a string passed in as "grant_type" in the response that will call this grantType
+     */
+    public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+    {
+        if (is_null($identifier) || is_numeric($identifier)) {
+            $identifier = $grantType->getQuerystringIdentifier();
+        }
+
+        $this->grantTypes[$identifier] = $grantType;
+    }
+
+    public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if ($this->revokeToken($request, $response)) {
+            $response->setStatusCode(200);
+            $response->addParameters(array('revoked' => true));
+        }
+    }
+
+    /**
+     * Revoke a refresh or access token. Returns true on success and when tokens are invalid
+     *
+     * Note: invalid tokens do not cause an error response since the client
+     * cannot handle such an error in a reasonable way.  Moreover, the
+     * purpose of the revocation request, invalidating the particular token,
+     * is already achieved.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool|null
+     */
+    public function revokeToken(RequestInterface $request, ResponseInterface $response)
+    {
+        if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+            $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
+            $response->addHttpHeaders(array('Allow' => 'POST'));
+
+            return null;
+        }
+
+        $token_type_hint = $request->request('token_type_hint');
+        if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) {
+            $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\'');
+
+            return null;
+        }
+
+        $token = $request->request('token');
+        if ($token === null) {
+            $response->setError(400, 'invalid_request', 'Missing token parameter to revoke');
+
+            return null;
+        }
+
+        // @todo remove this check for v2.0
+        if (!method_exists($this->accessToken, 'revokeToken')) {
+            $class = get_class($this->accessToken);
+            throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method");
+        }
+
+        $this->accessToken->revokeToken($token, $token_type_hint);
+
+        return true;
+    }
+}

+ 100 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/GrantType/AuthorizationCode.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\AuthorizationCodeInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements GrantTypeInterface
+{
+    protected $storage;
+    protected $authCode;
+
+    /**
+     * @param \OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information
+     */
+    public function __construct(AuthorizationCodeInterface $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    public function getQuerystringIdentifier()
+    {
+        return 'authorization_code';
+    }
+
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$request->request('code')) {
+            $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required');
+
+            return false;
+        }
+
+        $code = $request->request('code');
+        if (!$authCode = $this->storage->getAuthorizationCode($code)) {
+            $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client');
+
+            return false;
+        }
+
+        /*
+         * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request
+         * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3
+         */
+        if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) {
+            if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != urldecode($authCode['redirect_uri'])) {
+                $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3");
+
+                return false;
+            }
+        }
+
+        if (!isset($authCode['expires'])) {
+            throw new \Exception('Storage must return authcode with a value for "expires"');
+        }
+
+        if ($authCode["expires"] < time()) {
+            $response->setError(400, 'invalid_grant', "The authorization code has expired");
+
+            return false;
+        }
+
+        if (!isset($authCode['code'])) {
+            $authCode['code'] = $code; // used to expire the code after the access token is granted
+        }
+
+        $this->authCode = $authCode;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return $this->authCode['client_id'];
+    }
+
+    public function getScope()
+    {
+        return isset($this->authCode['scope']) ? $this->authCode['scope'] : null;
+    }
+
+    public function getUserId()
+    {
+        return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null;
+    }
+
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        $token = $accessToken->createAccessToken($client_id, $user_id, $scope);
+        $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+        return $token;
+    }
+}

+ 24 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/OpenID/ResponseType/CodeIdToken.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+class CodeIdToken implements CodeIdTokenInterface
+{
+    protected $authCode;
+    protected $idToken;
+
+    public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterface $idToken)
+    {
+        $this->authCode = $authCode;
+        $this->idToken = $idToken;
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        $result = $this->authCode->getAuthorizeResponse($params, $user_id);
+        $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id);
+        $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token'];
+
+        return $result;
+    }
+}

+ 124 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/OpenID/ResponseType/IdToken.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+
+class IdToken implements IdTokenInterface
+{
+    protected $userClaimsStorage;
+    protected $publicKeyStorage;
+    protected $config;
+    protected $encryptionUtil;
+
+    public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null)
+    {
+        $this->userClaimsStorage = $userClaimsStorage;
+        $this->publicKeyStorage = $publicKeyStorage;
+        if (is_null($encryptionUtil)) {
+            $encryptionUtil = new Jwt();
+        }
+        $this->encryptionUtil = $encryptionUtil;
+
+        if (!isset($config['issuer'])) {
+            throw new \LogicException('config parameter "issuer" must be set');
+        }
+        $this->config = array_merge(array(
+            'id_lifetime' => 3600,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $userInfo = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+        $params += array('scope' => null, 'state' => null, 'nonce' => null);
+
+        // create the id token.
+        list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+        $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']);
+
+        $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null);
+        $result["fragment"] = array('id_token' => $id_token);
+        if (isset($params['state'])) {
+            $result["fragment"]["state"] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null)
+    {
+        // pull auth_time from user info if supplied
+        list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+
+        $token = array(
+            'iss'        => $this->config['issuer'],
+            'sub'        => $user_id,
+            'aud'        => $client_id,
+            'iat'        => time(),
+            'exp'        => time() + $this->config['id_lifetime'],
+            'auth_time'  => $auth_time,
+        );
+
+        if ($nonce) {
+            $token['nonce'] = $nonce;
+        }
+
+        if ($userClaims) {
+            $token += $userClaims;
+        }
+
+        if ($access_token) {
+            $token['at_hash'] = $this->createAtHash($access_token, $client_id);
+        }
+
+        return $this->encodeToken($token, $client_id);
+    }
+
+    protected function createAtHash($access_token, $client_id = null)
+    {
+        // maps HS256 and RS256 to sha256, etc.
+        $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+        $hash_algorithm = 'sha' . substr($algorithm, 2);
+        $hash = hash($hash_algorithm, $access_token, true);
+        $at_hash = substr($hash, 0, strlen($hash) / 2);
+
+        return $this->encryptionUtil->urlSafeB64Encode($at_hash);
+    }
+
+    protected function encodeToken(array $token, $client_id = null)
+    {
+        $private_key = $this->publicKeyStorage->getPrivateKey($client_id);
+        $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+
+        return $this->encryptionUtil->encode($token, $private_key, $algorithm);
+    }
+
+    private function getUserIdAndAuthTime($userInfo)
+    {
+        $auth_time = null;
+
+        // support an array for user_id / auth_time
+        if (is_array($userInfo)) {
+            if (!isset($userInfo['user_id'])) {
+                throw new \LogicException('if $user_id argument is an array, user_id index must be set');
+            }
+
+            $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null;
+            $user_id = $userInfo['user_id'];
+        } else {
+            $user_id = $userInfo;
+        }
+
+        if (is_null($auth_time)) {
+            $auth_time = time();
+        }
+
+        // userInfo is a scalar, and so this is the $user_id. Auth Time is null
+        return array($user_id, $auth_time);
+    }
+}

+ 369 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Response.php

@@ -0,0 +1,369 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Class to handle OAuth2 Responses in a graceful way.  Use this interface
+ * to output the proper OAuth2 responses.
+ *
+ * @see OAuth2\ResponseInterface
+ *
+ * This class borrows heavily from the Symfony2 Framework and is part of the symfony package
+ * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
+ */
+class Response implements ResponseInterface
+{
+    public $version;
+    protected $statusCode = 200;
+    protected $statusText;
+    protected $parameters = array();
+    protected $httpHeaders = array();
+
+    public static $statusTexts = array(
+        100 => 'Continue',
+        101 => 'Switching Protocols',
+        200 => 'OK',
+        201 => 'Created',
+        202 => 'Accepted',
+        203 => 'Non-Authoritative Information',
+        204 => 'No Content',
+        205 => 'Reset Content',
+        206 => 'Partial Content',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        305 => 'Use Proxy',
+        307 => 'Temporary Redirect',
+        400 => 'Bad Request',
+        401 => 'Unauthorized',
+        402 => 'Payment Required',
+        403 => 'Forbidden',
+        404 => 'Not Found',
+        405 => 'Method Not Allowed',
+        406 => 'Not Acceptable',
+        407 => 'Proxy Authentication Required',
+        408 => 'Request Timeout',
+        409 => 'Conflict',
+        410 => 'Gone',
+        411 => 'Length Required',
+        412 => 'Precondition Failed',
+        413 => 'Request Entity Too Large',
+        414 => 'Request-URI Too Long',
+        415 => 'Unsupported Media Type',
+        416 => 'Requested Range Not Satisfiable',
+        417 => 'Expectation Failed',
+        418 => 'I\'m a teapot',
+        500 => 'Internal Server Error',
+        501 => 'Not Implemented',
+        502 => 'Bad Gateway',
+        503 => 'Service Unavailable',
+        504 => 'Gateway Timeout',
+        505 => 'HTTP Version Not Supported',
+    );
+
+    public function __construct($parameters = array(), $statusCode = 200, $headers = array())
+    {
+        $this->setParameters($parameters);
+        $this->setStatusCode($statusCode);
+        $this->setHttpHeaders($headers);
+        $this->version = '1.1';
+    }
+
+    /**
+     * Converts the response object to string containing all headers and the response content.
+     *
+     * @return string The response with headers and content
+     */
+    public function __toString()
+    {
+        $headers = array();
+        foreach ($this->httpHeaders as $name => $value) {
+            $headers[$name] = (array) $value;
+        }
+
+        return
+            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
+            $this->getHttpHeadersAsString($headers)."\r\n".
+            $this->getResponseBody();
+    }
+
+    /**
+     * Returns the build header line.
+     *
+     * @param string $name  The header name
+     * @param string $value The header value
+     *
+     * @return string The built header line
+     */
+    protected function buildHeader($name, $value)
+    {
+        return sprintf("%s: %s\n", $name, $value);
+    }
+
+    public function getStatusCode()
+    {
+        return $this->statusCode;
+    }
+
+    public function setStatusCode($statusCode, $text = null)
+    {
+        $this->statusCode = (int) $statusCode;
+        if ($this->isInvalid()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode));
+        }
+
+        $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
+    }
+
+    public function getStatusText()
+    {
+        return $this->statusText;
+    }
+
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+
+    public function setParameters(array $parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    public function addParameters(array $parameters)
+    {
+        $this->parameters = array_merge($this->parameters, $parameters);
+    }
+
+    public function getParameter($name, $default = null)
+    {
+        return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
+    }
+
+    public function setParameter($name, $value)
+    {
+        $this->parameters[$name] = $value;
+    }
+
+    public function setHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = $httpHeaders;
+    }
+
+    public function setHttpHeader($name, $value)
+    {
+        $this->httpHeaders[$name] = $value;
+    }
+
+    public function addHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders);
+    }
+
+    public function getHttpHeaders()
+    {
+        return $this->httpHeaders;
+    }
+
+    public function getHttpHeader($name, $default = null)
+    {
+        return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default;
+    }
+
+    public function getResponseBody($format = 'json')
+    {
+        switch ($format) {
+            case 'json':
+                return $this->parameters ? json_encode($this->parameters) : '';
+            case 'xml':
+                // this only works for single-level arrays
+                $xml = new \SimpleXMLElement('<response/>');
+                foreach ($this->parameters as $key => $param) {
+                    $xml->addChild($key, $param);
+                }
+
+                return $xml->asXML();
+        }
+
+        throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format));
+
+    }
+
+    public function send($format = 'json')
+    {
+        // headers have already been sent by the developer
+        if (headers_sent()) {
+            return;
+        }
+
+        switch ($format) {
+            case 'json':
+                $this->setHttpHeader('Content-Type', 'application/json');
+                break;
+            case 'xml':
+                $this->setHttpHeader('Content-Type', 'text/xml');
+                break;
+        }
+        // status
+        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
+
+        foreach ($this->getHttpHeaders() as $name => $header) {
+            header(sprintf('%s: %s', $name, $header));
+        }
+        echo $this->getResponseBody($format);
+    }
+
+    public function setError($statusCode, $error, $errorDescription = null, $errorUri = null)
+    {
+        $parameters = array(
+            'error' => $error,
+            'error_description' => $errorDescription,
+        );
+
+        if (!is_null($errorUri)) {
+            if (strlen($errorUri) > 0 && $errorUri[0] == '#') {
+                // we are referencing an oauth bookmark (for brevity)
+                $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri;
+            }
+            $parameters['error_uri'] = $errorUri;
+        }
+
+        $httpHeaders = array(
+            'Cache-Control' => 'no-store'
+        );
+
+        $this->setStatusCode($statusCode);
+        $this->addParameters($parameters);
+        $this->addHttpHeaders($httpHeaders);
+
+        if (!$this->isClientError() && !$this->isServerError()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode));
+        }
+    }
+
+    public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null)
+    {
+        if (empty($url)) {
+            throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
+        }
+
+        $parameters = array();
+
+        if (!is_null($state)) {
+            $parameters['state'] = $state;
+        }
+
+        if (!is_null($error)) {
+            $this->setError(400, $error, $errorDescription, $errorUri);
+        }
+        $this->setStatusCode($statusCode);
+        $this->addParameters($parameters);
+
+        if (count($this->parameters) > 0) {
+            // add parameters to URL redirection
+            $parts = parse_url($url);
+            $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?';
+            $url .= $sep . http_build_query($this->parameters);
+        }
+
+        $this->addHttpHeaders(array('Location' =>  $url));
+
+        if (!$this->isRedirection()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode));
+        }
+    }
+
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isInvalid()
+    {
+        return $this->statusCode < 100 || $this->statusCode >= 600;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isInformational()
+    {
+        return $this->statusCode >= 100 && $this->statusCode < 200;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isSuccessful()
+    {
+        return $this->statusCode >= 200 && $this->statusCode < 300;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isRedirection()
+    {
+        return $this->statusCode >= 300 && $this->statusCode < 400;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isClientError()
+    {
+        return $this->statusCode >= 400 && $this->statusCode < 500;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isServerError()
+    {
+        return $this->statusCode >= 500 && $this->statusCode < 600;
+    }
+
+    /*
+     * Functions from Symfony2 HttpFoundation - output pretty header
+     */
+    private function getHttpHeadersAsString($headers)
+    {
+        if (count($headers) == 0) {
+            return '';
+        }
+
+        $max = max(array_map('strlen', array_keys($headers))) + 1;
+        $content = '';
+        ksort($headers);
+        foreach ($headers as $name => $values) {
+            foreach ($values as $value) {
+                $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value);
+            }
+        }
+
+        return $content;
+    }
+
+    private function beautifyHeaderName($name)
+    {
+        return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name));
+    }
+
+    private function beautifyCallback($match)
+    {
+        return '-'.strtoupper($match[1]);
+    }
+}

+ 194 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/ResponseType/AccessToken.php

@@ -0,0 +1,194 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface;
+use OAuth2\Storage\RefreshTokenInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AccessToken implements AccessTokenInterface
+{
+    protected $tokenStorage;
+    protected $refreshStorage;
+    protected $config;
+
+    /**
+     * @param OAuth2\Storage\AccessTokenInterface  $tokenStorage   REQUIRED Storage class for saving access token information
+     * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information
+     * @param array                                $config         OPTIONAL Configuration options for the server
+     *                                                             <code>
+     *                                                             $config = array(
+     *                                                             'token_type' => 'bearer',              // token type identifier
+     *                                                             'access_lifetime' => 3600,             // time before access token expires
+     *                                                             'refresh_token_lifetime' => 1209600,   // time before refresh token expires
+     *                                                             );
+     *                                                             </endcode>
+     */
+    public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array())
+    {
+        $this->tokenStorage = $tokenStorage;
+        $this->refreshStorage = $refreshStorage;
+
+        $this->config = array_merge(array(
+            'token_type'             => 'bearer',
+            'access_lifetime'        => 3600,
+            'refresh_token_lifetime' => 1209600,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+
+        $params += array('scope' => null, 'state' => null);
+
+        /*
+         * a refresh token MUST NOT be included in the fragment
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-4.2.2
+         */
+        $includeRefreshToken = false;
+        $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken);
+
+        if (isset($params['state'])) {
+            $result["fragment"]["state"] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    /**
+     * Handle the creation of access token, also issue refresh token if supported / desirable.
+     *
+     * @param $client_id                client identifier related to the access token.
+     * @param $user_id                  user ID associated with the access token
+     * @param $scope                    OPTIONAL scopes to be stored in space-separated string.
+     * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-5
+     * @ingroup oauth2_section_5
+     */
+    public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true)
+    {
+        $token = array(
+            "access_token" => $this->generateAccessToken(),
+            "expires_in" => $this->config['access_lifetime'],
+            "token_type" => $this->config['token_type'],
+            "scope" => $scope
+        );
+
+        $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope);
+
+        /*
+         * Issue a refresh token also, if we support them
+         *
+         * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface
+         * is supplied in the constructor
+         */
+        if ($includeRefreshToken && $this->refreshStorage) {
+            $token["refresh_token"] = $this->generateRefreshToken();
+            $expires = 0;
+            if ($this->config['refresh_token_lifetime'] > 0) {
+                $expires = time() + $this->config['refresh_token_lifetime'];
+            }
+            $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope);
+        }
+
+        return $token;
+    }
+
+    /**
+     * Generates an unique access token.
+     *
+     * Implementing classes may want to override this function to implement
+     * other access token generation schemes.
+     *
+     * @return
+     * An unique access token.
+     *
+     * @ingroup oauth2_section_4
+     */
+    protected function generateAccessToken()
+    {
+        if (function_exists('openssl_random_pseudo_bytes')) {
+            $randomData = openssl_random_pseudo_bytes(20);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        if (function_exists('mcrypt_create_iv')) {
+            $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+            $randomData = file_get_contents('/dev/urandom', false, null, 0, 20);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        // Last resort which you probably should just get rid of:
+        $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
+
+        return substr(hash('sha512', $randomData), 0, 40);
+    }
+
+    /**
+     * Generates an unique refresh token
+     *
+     * Implementing classes may want to override this function to implement
+     * other refresh token generation schemes.
+     *
+     * @return
+     * An unique refresh.
+     *
+     * @ingroup oauth2_section_4
+     * @see OAuth2::generateAccessToken()
+     */
+    protected function generateRefreshToken()
+    {
+        return $this->generateAccessToken(); // let's reuse the same scheme for token generation
+    }
+
+    /**
+     * Handle the revoking of refresh tokens, and access tokens if supported / desirable
+     * RFC7009 specifies that "If the server is unable to locate the token using
+     * the given hint, it MUST extend its search across all of its supported token types"
+     *
+     * @param $token
+     * @param null $tokenTypeHint
+     * @return boolean
+     */
+    public function revokeToken($token, $tokenTypeHint = null)
+    {
+        if ($tokenTypeHint == 'refresh_token') {
+            if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) {
+                return true;
+            }
+        }
+
+        /** @TODO remove in v2 */
+        if (!method_exists($this->tokenStorage, 'unsetAccessToken')) {
+            throw new \RuntimeException(
+                sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage)
+            ));
+        }
+
+        $revoked = $this->tokenStorage->unsetAccessToken($token);
+
+        // if a typehint is supplied and fails, try other storages 
+        // @see https://tools.ietf.org/html/rfc7009#section-2.1
+        if (!$revoked && $tokenTypeHint != 'refresh_token') {
+            if ($this->refreshStorage) {
+                $revoked = $this->refreshStorage->unsetRefreshToken($token);
+            }
+        }
+
+        return $revoked;
+    }
+}

+ 100 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/ResponseType/AuthorizationCode.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements AuthorizationCodeInterface
+{
+    protected $storage;
+    protected $config;
+
+    public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+    {
+        $this->storage = $storage;
+        $this->config = array_merge(array(
+            'enforce_redirect' => false,
+            'auth_code_lifetime' => 30,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+
+        $params += array('scope' => null, 'state' => null);
+
+        $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']);
+
+        if (isset($params['state'])) {
+            $result['query']['state'] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    /**
+     * Handle the creation of the authorization code.
+     *
+     * @param $client_id
+     * Client identifier related to the authorization code
+     * @param $user_id
+     * User ID associated with the authorization code
+     * @param $redirect_uri
+     * An absolute URI to which the authorization server will redirect the
+     * user-agent to when the end-user authorization step is completed.
+     * @param $scope
+     * (optional) Scopes to be stored in space-separated string.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @ingroup oauth2_section_4
+     */
+    public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null)
+    {
+        $code = $this->generateAuthorizationCode();
+        $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope);
+
+        return $code;
+    }
+
+    /**
+     * @return
+     * TRUE if the grant type requires a redirect_uri, FALSE if not
+     */
+    public function enforceRedirect()
+    {
+        return $this->config['enforce_redirect'];
+    }
+
+    /**
+     * Generates an unique auth code.
+     *
+     * Implementing classes may want to override this function to implement
+     * other auth code generation schemes.
+     *
+     * @return
+     * An unique auth code.
+     *
+     * @ingroup oauth2_section_4
+     */
+    protected function generateAuthorizationCode()
+    {
+        $tokenLen = 40;
+        if (function_exists('openssl_random_pseudo_bytes')) {
+            $randomData = openssl_random_pseudo_bytes(100);
+        } elseif (function_exists('mcrypt_create_iv')) {
+            $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM);
+        } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+            $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true);
+        } else {
+            $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
+        }
+
+        return substr(hash('sha512', $randomData), 0, $tokenLen);
+    }
+}

+ 879 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Server.php

@@ -0,0 +1,879 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Controller\ResourceControllerInterface;
+use OAuth2\Controller\ResourceController;
+use OAuth2\OpenID\Controller\UserInfoControllerInterface;
+use OAuth2\OpenID\Controller\UserInfoController;
+use OAuth2\OpenID\Controller\AuthorizeController as OpenIDAuthorizeController;
+use OAuth2\OpenID\ResponseType\AuthorizationCode as OpenIDAuthorizationCodeResponseType;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+use OAuth2\OpenID\GrantType\AuthorizationCode as OpenIDAuthorizationCodeGrantType;
+use OAuth2\Controller\AuthorizeControllerInterface;
+use OAuth2\Controller\AuthorizeController;
+use OAuth2\Controller\TokenControllerInterface;
+use OAuth2\Controller\TokenController;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\ClientAssertionType\HttpBasic;
+use OAuth2\ResponseType\ResponseTypeInterface;
+use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType;
+use OAuth2\ResponseType\AccessToken;
+use OAuth2\ResponseType\JwtAccessToken;
+use OAuth2\OpenID\ResponseType\CodeIdToken;
+use OAuth2\OpenID\ResponseType\IdToken;
+use OAuth2\OpenID\ResponseType\IdTokenToken;
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\TokenType\Bearer;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\GrantType\UserCredentials;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\GrantType\RefreshToken;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage;
+use OAuth2\Storage\JwtAccessTokenInterface;
+
+/**
+* Server class for OAuth2
+* This class serves as a convience class which wraps the other Controller classes
+*
+* @see OAuth2\Controller\ResourceController
+* @see OAuth2\Controller\AuthorizeController
+* @see OAuth2\Controller\TokenController
+*/
+class Server implements ResourceControllerInterface,
+    AuthorizeControllerInterface,
+    TokenControllerInterface,
+    UserInfoControllerInterface
+{
+    // misc properties
+    /**
+     * @var Response
+     */
+    protected $response;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var array
+     */
+    protected $storages;
+
+    // servers
+    /**
+     * @var AuthorizeControllerInterface
+     */
+    protected $authorizeController;
+
+    /**
+     * @var TokenControllerInterface
+     */
+    protected $tokenController;
+
+    /**
+     * @var ResourceControllerInterface
+     */
+    protected $resourceController;
+
+    /**
+     * @var UserInfoControllerInterface
+     */
+    protected $userInfoController;
+
+    // config classes
+    protected $grantTypes;
+    protected $responseTypes;
+    protected $tokenType;
+
+    /**
+     * @var ScopeInterface
+     */
+    protected $scopeUtil;
+    protected $clientAssertionType;
+
+    protected $storageMap = array(
+        'access_token' => 'OAuth2\Storage\AccessTokenInterface',
+        'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface',
+        'client_credentials' => 'OAuth2\Storage\ClientCredentialsInterface',
+        'client' => 'OAuth2\Storage\ClientInterface',
+        'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface',
+        'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface',
+        'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface',
+        'public_key' => 'OAuth2\Storage\PublicKeyInterface',
+        'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface',
+        'scope' => 'OAuth2\Storage\ScopeInterface',
+    );
+
+    protected $responseTypeMap = array(
+        'token' => 'OAuth2\ResponseType\AccessTokenInterface',
+        'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface',
+        'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface',
+        'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface',
+        'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface',
+    );
+
+    /**
+     * @param mixed                                                   $storage             (array or OAuth2\Storage) - single object or array of objects implementing the
+     *                                                                                     required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum)
+     * @param array                                                   $config              specify a different token lifetime, token header name, etc
+     * @param array                                                   $grantTypes          An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens
+     * @param array                                                   $responseTypes       Response types to use.  array keys should be "code" and and "token" for
+     *                                                                                     Access Token and Authorization Code response types
+     * @param \OAuth2\TokenType\TokenTypeInterface                     $tokenType           The token type object to use. Valid token types are "bearer" and "mac"
+     * @param \OAuth2\ScopeInterface                                   $scopeUtil           The scope utility class to use to validate scope
+     * @param \OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity.  Default is HttpBasic
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null)
+    {
+        $storage = is_array($storage) ? $storage : array($storage);
+        $this->storages = array();
+        foreach ($storage as $key => $service) {
+            $this->addStorage($service, $key);
+        }
+
+        // merge all config values.  These get passed to our controller objects
+        $this->config = array_merge(array(
+            'use_jwt_access_tokens'        => false,
+            'store_encrypted_token_string' => true,
+            'use_openid_connect'       => false,
+            'id_lifetime'              => 3600,
+            'access_lifetime'          => 3600,
+            'www_realm'                => 'Service',
+            'token_param_name'         => 'access_token',
+            'token_bearer_header_name' => 'Bearer',
+            'enforce_state'            => true,
+            'require_exact_redirect_uri' => true,
+            'allow_implicit'           => false,
+            'allow_credentials_in_request_body' => true,
+            'allow_public_clients'     => true,
+            'always_issue_new_refresh_token' => false,
+            'unset_refresh_token_after_use' => true,
+        ), $config);
+
+        foreach ($grantTypes as $key => $grantType) {
+            $this->addGrantType($grantType, $key);
+        }
+
+        foreach ($responseTypes as $key => $responseType) {
+            $this->addResponseType($responseType, $key);
+        }
+
+        $this->tokenType = $tokenType;
+        $this->scopeUtil = $scopeUtil;
+        $this->clientAssertionType = $clientAssertionType;
+
+        if ($this->config['use_openid_connect']) {
+            $this->validateOpenIdConnect();
+        }
+    }
+
+    public function getAuthorizeController()
+    {
+        if (is_null($this->authorizeController)) {
+            $this->authorizeController = $this->createDefaultAuthorizeController();
+        }
+
+        return $this->authorizeController;
+    }
+
+    public function getTokenController()
+    {
+        if (is_null($this->tokenController)) {
+            $this->tokenController = $this->createDefaultTokenController();
+        }
+
+        return $this->tokenController;
+    }
+
+    public function getResourceController()
+    {
+        if (is_null($this->resourceController)) {
+            $this->resourceController = $this->createDefaultResourceController();
+        }
+
+        return $this->resourceController;
+    }
+
+    public function getUserInfoController()
+    {
+        if (is_null($this->userInfoController)) {
+            $this->userInfoController = $this->createDefaultUserInfoController();
+        }
+
+        return $this->userInfoController;
+    }
+
+    /**
+     * every getter deserves a setter
+     *
+     * @param AuthorizeControllerInterface $authorizeController
+     */
+    public function setAuthorizeController(AuthorizeControllerInterface $authorizeController)
+    {
+        $this->authorizeController = $authorizeController;
+    }
+
+    /**
+     * every getter deserves a setter
+     *
+     * @param TokenControllerInterface $tokenController
+     */
+    public function setTokenController(TokenControllerInterface $tokenController)
+    {
+        $this->tokenController = $tokenController;
+    }
+
+    /**
+     * every getter deserves a setter
+     *
+     * @param ResourceControllerInterface $resourceController
+     */
+    public function setResourceController(ResourceControllerInterface $resourceController)
+    {
+        $this->resourceController = $resourceController;
+    }
+
+    /**
+     * every getter deserves a setter
+     *
+     * @param UserInfoControllerInterface $userInfoController
+     */
+    public function setUserInfoController(UserInfoControllerInterface $userInfoController)
+    {
+        $this->userInfoController = $userInfoController;
+    }
+
+    /**
+     * Return claims about the authenticated end-user.
+     * This would be called from the "/UserInfo" endpoint as defined in the spec.
+     *
+     * @param $request - \OAuth2\RequestInterface
+     * Request object to grant access token
+     *
+     * @param $response - \OAuth2\ResponseInterface
+     * Response object containing error messages (failure) or user claims (success)
+     *
+     * @return ResponseInterface
+     *
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     *
+     * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
+     */
+    public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getUserInfoController()->handleUserInfoRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    /**
+     * Grant or deny a requested access token.
+     * This would be called from the "/token" endpoint as defined in the spec.
+     * Obviously, you can call your endpoint whatever you want.
+     *
+     * @param $request - \OAuth2\RequestInterface
+     * Request object to grant access token
+     *
+     * @param $response - \OAuth2\ResponseInterface
+     * Response object containing error messages (failure) or access token (success)
+     *
+     * @return ResponseInterface
+     *
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @see http://tools.ietf.org/html/rfc6749#section-10.6
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function handleTokenRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getTokenController()->handleTokenRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getTokenController()->grantAccessToken($request, $this->response);
+
+        return $value;
+    }
+
+    /**
+     * Handle a revoke token request
+     * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec
+     *
+     * @see https://tools.ietf.org/html/rfc7009#section-2
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return Response|ResponseInterface
+     */
+    public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getTokenController()->handleRevokeRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    /**
+     * Redirect the user appropriately after approval.
+     *
+     * After the user has approved or denied the resource request the
+     * authorization server should call this function to redirect the user
+     * appropriately.
+     *
+     * @param $request
+     * The request should have the follow parameters set in the querystring:
+     * - response_type: The requested response: an access token, an
+     * authorization code, or both.
+     * - client_id: The client identifier as described in Section 2.
+     * - redirect_uri: An absolute URI to which the authorization server
+     * will redirect the user-agent to when the end-user authorization
+     * step is completed.
+     * - scope: (optional) The scope of the resource request expressed as a
+     * list of space-delimited strings.
+     * - state: (optional) An opaque value used by the client to maintain
+     * state between the request and callback.
+     * @param ResponseInterface $response
+     * @param $is_authorized
+     * TRUE or FALSE depending on whether the user authorized the access.
+     * @param $user_id
+     * Identifier of user who authorized the client
+     *
+     * @return Response
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+    {
+        $this->response = $response;
+        $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id);
+
+        return $this->response;
+    }
+
+    /**
+     * Pull the authorization request data out of the HTTP request.
+     * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it
+     * by setting $config['enforce_redirect'] to true.
+     * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that
+     * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true.
+     *
+     * The draft specifies that the parameters should be retrieved from GET, override the Response
+     * object to change this
+     *
+     * @return
+     * The authorization parameters so the authorization server can prompt
+     * the user for approval if valid.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.1
+     * @see http://tools.ietf.org/html/rfc6749#section-10.12
+     *
+     * @ingroup oauth2_section_3
+     */
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response);
+
+        return $value;
+    }
+
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope);
+
+        return $value;
+    }
+
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getResourceController()->getAccessTokenData($request, $this->response);
+
+        return $value;
+    }
+
+    public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+    {
+        if (!is_string($identifier)) {
+            $identifier = $grantType->getQuerystringIdentifier();
+        }
+
+        $this->grantTypes[$identifier] = $grantType;
+
+        // persist added grant type down to TokenController
+        if (!is_null($this->tokenController)) {
+            $this->getTokenController()->addGrantType($grantType, $identifier);
+        }
+    }
+
+    /**
+     * Set a storage object for the server
+     *
+     * @param $storage
+     * An object implementing one of the Storage interfaces
+     * @param $key
+     * If null, the storage is set to the key of each storage interface it implements
+     *
+     * @see storageMap
+     */
+    public function addStorage($storage, $key = null)
+    {
+        // if explicitly set to a valid key, do not "magically" set below
+        if (isset($this->storageMap[$key])) {
+            if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) {
+                throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key]));
+            }
+            $this->storages[$key] = $storage;
+
+            // special logic to handle "client" and "client_credentials" strangeness
+            if ($key === 'client' && !isset($this->storages['client_credentials'])) {
+                if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) {
+                    $this->storages['client_credentials'] = $storage;
+                }
+            } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) {
+                if ($storage instanceof \OAuth2\Storage\ClientInterface) {
+                    $this->storages['client'] = $storage;
+                }
+            }
+        } elseif (!is_null($key) && !is_numeric($key)) {
+            throw new \InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap))));
+        } else {
+            $set = false;
+            foreach ($this->storageMap as $type => $interface) {
+                if ($storage instanceof $interface) {
+                    $this->storages[$type] = $storage;
+                    $set = true;
+                }
+            }
+
+            if (!$set) {
+                throw new \InvalidArgumentException(sprintf('storage of class "%s" must implement one of [%s]', get_class($storage), implode(', ', $this->storageMap)));
+            }
+        }
+    }
+
+    public function addResponseType(ResponseTypeInterface $responseType, $key = null)
+    {
+        $key = $this->normalizeResponseType($key);
+
+        if (isset($this->responseTypeMap[$key])) {
+            if (!$responseType instanceof $this->responseTypeMap[$key]) {
+                throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key]));
+            }
+            $this->responseTypes[$key] = $responseType;
+        } elseif (!is_null($key) && !is_numeric($key)) {
+            throw new \InvalidArgumentException(sprintf('unknown responseType key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->responseTypeMap))));
+        } else {
+            $set = false;
+            foreach ($this->responseTypeMap as $type => $interface) {
+                if ($responseType instanceof $interface) {
+                    $this->responseTypes[$type] = $responseType;
+                    $set = true;
+                }
+            }
+
+            if (!$set) {
+                throw new \InvalidArgumentException(sprintf('Unknown response type %s.  Please implement one of [%s]', get_class($responseType), implode(', ', $this->responseTypeMap)));
+            }
+        }
+    }
+
+    public function getScopeUtil()
+    {
+        if (!$this->scopeUtil) {
+            $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null;
+            $this->scopeUtil = new Scope($storage);
+        }
+
+        return $this->scopeUtil;
+    }
+
+    /**
+     * every getter deserves a setter
+     *
+     * @param ScopeInterface $scopeUtil
+     */
+    public function setScopeUtil($scopeUtil)
+    {
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    protected function createDefaultAuthorizeController()
+    {
+        if (!isset($this->storages['client'])) {
+            throw new \LogicException('You must supply a storage object implementing \OAuth2\Storage\ClientInterface to use the authorize server');
+        }
+        if (0 == count($this->responseTypes)) {
+            $this->responseTypes = $this->getDefaultResponseTypes();
+        }
+        if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) {
+            $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType();
+            if ($this->config['allow_implicit']) {
+                $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType();
+            }
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri')));
+
+        if ($this->config['use_openid_connect']) {
+            return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
+        }
+
+        return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
+    }
+
+    protected function createDefaultTokenController()
+    {
+        if (0 == count($this->grantTypes)) {
+            $this->grantTypes = $this->getDefaultGrantTypes();
+        }
+
+        if (is_null($this->clientAssertionType)) {
+            // see if HttpBasic assertion type is requred.  If so, then create it from storage classes.
+            foreach ($this->grantTypes as $grantType) {
+                if (!$grantType instanceof ClientAssertionTypeInterface) {
+                    if (!isset($this->storages['client_credentials'])) {
+                        throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server');
+                    }
+                    $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients')));
+                    $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config);
+                    break;
+                }
+            }
+        }
+
+        if (!isset($this->storages['client'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server');
+        }
+
+        $accessTokenResponseType = $this->getAccessTokenResponseType();
+
+        return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil());
+    }
+
+    protected function createDefaultResourceController()
+    {
+        if ($this->config['use_jwt_access_tokens']) {
+            // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set
+            if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) {
+                $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage();
+            }
+        } elseif (!isset($this->storages['access_token'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server');
+        }
+
+        if (!$this->tokenType) {
+            $this->tokenType = $this->getDefaultTokenType();
+        }
+
+        $config = array_intersect_key($this->config, array('www_realm' => ''));
+
+        return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil());
+    }
+
+    protected function createDefaultUserInfoController()
+    {
+        if ($this->config['use_jwt_access_tokens']) {
+            // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set
+            if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) {
+                $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage();
+            }
+        } elseif (!isset($this->storages['access_token'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server');
+        }
+
+        if (!isset($this->storages['user_claims'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server');
+        }
+
+        if (!$this->tokenType) {
+            $this->tokenType = $this->getDefaultTokenType();
+        }
+
+        $config = array_intersect_key($this->config, array('www_realm' => ''));
+
+        return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil());
+    }
+
+    protected function getDefaultTokenType()
+    {
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name')));
+
+        return new Bearer($config);
+    }
+
+    protected function getDefaultResponseTypes()
+    {
+        $responseTypes = array();
+
+        if ($this->config['allow_implicit']) {
+            $responseTypes['token'] = $this->getAccessTokenResponseType();
+        }
+
+        if ($this->config['use_openid_connect']) {
+            $responseTypes['id_token'] = $this->getIdTokenResponseType();
+            if ($this->config['allow_implicit']) {
+                $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType();
+            }
+        }
+
+        if (isset($this->storages['authorization_code'])) {
+            $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime')));
+            if ($this->config['use_openid_connect']) {
+                if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+                    throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true');
+                }
+                $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+                $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']);
+            } else {
+                $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+            }
+        }
+
+        if (count($responseTypes) == 0) {
+            throw new \LogicException('You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set "allow_implicit" to true and implement a OAuth2\Storage\AccessTokenInterface storage object');
+        }
+
+        return $responseTypes;
+    }
+
+    protected function getDefaultGrantTypes()
+    {
+        $grantTypes = array();
+
+        if (isset($this->storages['user_credentials'])) {
+            $grantTypes['password'] = new UserCredentials($this->storages['user_credentials']);
+        }
+
+        if (isset($this->storages['client_credentials'])) {
+            $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => ''));
+            $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config);
+        }
+
+        if (isset($this->storages['refresh_token'])) {
+            $config = array_intersect_key($this->config, array_flip(explode(' ', 'always_issue_new_refresh_token unset_refresh_token_after_use')));
+            $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config);
+        }
+
+        if (isset($this->storages['authorization_code'])) {
+            if ($this->config['use_openid_connect']) {
+                if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+                    throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true');
+                }
+                $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']);
+            } else {
+                $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']);
+            }
+        }
+
+        if (count($grantTypes) == 0) {
+            throw new \LogicException('Unable to build default grant types - You must supply an array of grant_types in the constructor');
+        }
+
+        return $grantTypes;
+    }
+
+    protected function getAccessTokenResponseType()
+    {
+        if (isset($this->responseTypes['token'])) {
+            return $this->responseTypes['token'];
+        }
+
+        if ($this->config['use_jwt_access_tokens']) {
+            return $this->createDefaultJwtAccessTokenResponseType();
+        }
+
+        return $this->createDefaultAccessTokenResponseType();
+    }
+
+    protected function getIdTokenResponseType()
+    {
+        if (isset($this->responseTypes['id_token'])) {
+            return $this->responseTypes['id_token'];
+        }
+
+        return $this->createDefaultIdTokenResponseType();
+    }
+
+    protected function getIdTokenTokenResponseType()
+    {
+        if (isset($this->responseTypes['id_token token'])) {
+            return $this->responseTypes['id_token token'];
+        }
+
+        return $this->createDefaultIdTokenTokenResponseType();
+    }
+
+    /**
+     * For Resource Controller
+     */
+    protected function createDefaultJwtAccessTokenStorage()
+    {
+        if (!isset($this->storages['public_key'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens');
+        }
+        $tokenStorage = null;
+        if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) {
+            $tokenStorage = $this->storages['access_token'];
+        }
+        // wrap the access token storage as required.
+        return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage);
+    }
+
+    /**
+     * For Authorize and Token Controllers
+     */
+    protected function createDefaultJwtAccessTokenResponseType()
+    {
+        if (!isset($this->storages['public_key'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens');
+        }
+
+        $tokenStorage = null;
+        if (isset($this->storages['access_token'])) {
+            $tokenStorage = $this->storages['access_token'];
+        }
+
+        $refreshStorage = null;
+        if (isset($this->storages['refresh_token'])) {
+            $refreshStorage = $this->storages['refresh_token'];
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime')));
+
+        return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config);
+    }
+
+    protected function createDefaultAccessTokenResponseType()
+    {
+        if (!isset($this->storages['access_token'])) {
+            throw new \LogicException('You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server');
+        }
+
+        $refreshStorage = null;
+        if (isset($this->storages['refresh_token'])) {
+            $refreshStorage = $this->storages['refresh_token'];
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime')));
+        $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() :  $this->getDefaultTokenType()->getTokenType();
+
+        return new AccessToken($this->storages['access_token'], $refreshStorage, $config);
+    }
+
+    protected function createDefaultIdTokenResponseType()
+    {
+        if (!isset($this->storages['user_claims'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect');
+        }
+        if (!isset($this->storages['public_key'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect');
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime')));
+
+        return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config);
+    }
+
+    protected function createDefaultIdTokenTokenResponseType()
+    {
+        return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType());
+    }
+
+    protected function validateOpenIdConnect()
+    {
+        $authCodeGrant = $this->getGrantType('authorization_code');
+        if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) {
+            throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.');
+        }
+    }
+
+    protected function normalizeResponseType($name)
+    {
+        // for multiple-valued response types - make them alphabetical
+        if (!empty($name) && false !== strpos($name, ' ')) {
+            $types = explode(' ', $name);
+            sort($types);
+            $name = implode(' ', $types);
+        }
+
+        return $name;
+    }
+
+    public function getResponse()
+    {
+        return $this->response;
+    }
+
+    public function getStorages()
+    {
+        return $this->storages;
+    }
+
+    public function getStorage($name)
+    {
+        return isset($this->storages[$name]) ? $this->storages[$name] : null;
+    }
+
+    public function getGrantTypes()
+    {
+        return $this->grantTypes;
+    }
+
+    public function getGrantType($name)
+    {
+        return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null;
+    }
+
+    public function getResponseTypes()
+    {
+        return $this->responseTypes;
+    }
+
+    public function getResponseType($name)
+    {
+        // for multiple-valued response types - make them alphabetical
+        $name = $this->normalizeResponseType($name);
+
+        return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null;
+    }
+
+    public function getTokenType()
+    {
+        return $this->tokenType;
+    }
+
+    public function getClientAssertionType()
+    {
+        return $this->clientAssertionType;
+    }
+
+    public function setConfig($name, $value)
+    {
+        $this->config[$name] = $value;
+    }
+
+    public function getConfig($name, $default = null)
+    {
+        return isset($this->config[$name]) ? $this->config[$name] : $default;
+    }
+}

+ 64 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/AccessTokenInterface.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save access tokens
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AccessTokenInterface
+{
+    /**
+     * Look up the supplied oauth_token from storage.
+     *
+     * We need to retrieve access token data as we create and verify tokens.
+     *
+     * @param $oauth_token
+     * oauth_token to be check with.
+     *
+     * @return
+     * An associative array as below, and return NULL if the supplied oauth_token
+     * is invalid:
+     * - expires: Stored expiration in unix timestamp.
+     * - client_id: (optional) Stored client identifier.
+     * - user_id: (optional) Stored user identifier.
+     * - scope: (optional) Stored scope values in space-separated string.
+     * - id_token: (optional) Stored id_token (if "use_openid_connect" is true).
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function getAccessToken($oauth_token);
+
+    /**
+     * Store the supplied access token values to storage.
+     *
+     * We need to store access token data as we create and verify tokens.
+     *
+     * @param $oauth_token    oauth_token to be stored.
+     * @param $client_id      client identifier to be stored.
+     * @param $user_id        user identifier to be stored.
+     * @param int    $expires expiration to be stored as a Unix timestamp.
+     * @param string $scope   OPTIONAL Scopes to be stored in space-separated string.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null);
+
+    /**
+     * Expire an access token.
+     *
+     * This is not explicitly required in the spec, but if defined in a draft RFC for token
+     * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009
+     *
+     * @param $access_token
+     * Access token to be expired.
+     *
+     * @return BOOL true if an access token was unset, false if not
+     * @ingroup oauth2_section_6
+     *
+     * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x
+     */
+    //public function unsetAccessToken($access_token);
+}

+ 86 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/AuthorizationCodeInterface.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save authorization codes for the "Authorization Code"
+ * grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface
+{
+    /**
+     * The Authorization Code grant type supports a response type of "code".
+     *
+     * @var string
+     * @see http://tools.ietf.org/html/rfc6749#section-1.4.1
+     * @see http://tools.ietf.org/html/rfc6749#section-4.2
+     */
+    const RESPONSE_TYPE_CODE = "code";
+
+    /**
+     * Fetch authorization code data (probably the most common grant type).
+     *
+     * Retrieve the stored data for the given authorization code.
+     *
+     * Required for OAuth2::GRANT_TYPE_AUTH_CODE.
+     *
+     * @param $code
+     * Authorization code to be check with.
+     *
+     * @return
+     * An associative array as below, and NULL if the code is invalid
+     * @code
+     * return array(
+     *     "client_id"    => CLIENT_ID,      // REQUIRED Stored client identifier
+     *     "user_id"      => USER_ID,        // REQUIRED Stored user identifier
+     *     "expires"      => EXPIRES,        // REQUIRED Stored expiration in unix timestamp
+     *     "redirect_uri" => REDIRECT_URI,   // REQUIRED Stored redirect URI
+     *     "scope"        => SCOPE,          // OPTIONAL Stored scope values in space-separated string
+     * );
+     * @endcode
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function getAuthorizationCode($code);
+
+    /**
+     * Take the provided authorization code values and store them somewhere.
+     *
+     * This function should be the storage counterpart to getAuthCode().
+     *
+     * If storage fails for some reason, we're not currently checking for
+     * any sort of success/failure, so you should bail out of the script
+     * and provide a descriptive fail message.
+     *
+     * Required for OAuth2::GRANT_TYPE_AUTH_CODE.
+     *
+     * @param string $code         Authorization code to be stored.
+     * @param mixed  $client_id    Client identifier to be stored.
+     * @param mixed  $user_id      User identifier to be stored.
+     * @param string $redirect_uri Redirect URI(s) to be stored in a space-separated string.
+     * @param int    $expires      Expiration to be stored as a Unix timestamp.
+     * @param string $scope        OPTIONAL Scopes to be stored in space-separated string.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null);
+
+    /**
+     * once an Authorization Code is used, it must be expired
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.2
+     *
+     *    The client MUST NOT use the authorization code
+     *    more than once.  If an authorization code is used more than
+     *    once, the authorization server MUST deny the request and SHOULD
+     *    revoke (when possible) all tokens previously issued based on
+     *    that authorization code
+     *
+     */
+    public function expireAuthorizationCode($code);
+}

+ 480 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Cassandra.php

@@ -0,0 +1,480 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use phpcassa\ColumnFamily;
+use phpcassa\ColumnSlice;
+use phpcassa\Connection\ConnectionPool;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Cassandra storage for all storage types
+ *
+ * To use, install "thobbs/phpcassa" via composer
+ * <code>
+ *  composer require thobbs/phpcassa:dev-master
+ * </code>
+ *
+ * Once this is done, instantiate the
+ * <code>
+ *  $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160'));
+ * </code>
+ *
+ * Then, register the storage client:
+ * <code>
+ *  $storage = new OAuth2\Storage\Cassandra($cassandra);
+ *  $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
+ * </code>
+ *
+ * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage
+ */
+class Cassandra implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+
+    private $cache;
+
+    /* The cassandra client */
+    protected $cassandra;
+
+    /* Configuration array */
+    protected $config;
+
+    /**
+     * Cassandra Storage! uses phpCassa
+     *
+     * @param \phpcassa\ConnectionPool $cassandra
+     * @param array                    $config
+     */
+    public function __construct($connection = array(), array $config = array())
+    {
+        if ($connection instanceof ConnectionPool) {
+            $this->cassandra = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array');
+            }
+            $connection = array_merge(array(
+                'keyspace' => 'oauth2',
+                'servers'  => null,
+            ), $connection);
+
+            $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']);
+        }
+
+        $this->config = array_merge(array(
+            // cassandra config
+            'column_family' => 'auth',
+
+            // key names
+            'client_key' => 'oauth_clients:',
+            'access_token_key' => 'oauth_access_tokens:',
+            'refresh_token_key' => 'oauth_refresh_tokens:',
+            'code_key' => 'oauth_authorization_codes:',
+            'user_key' => 'oauth_users:',
+            'jwt_key' => 'oauth_jwt:',
+            'scope_key' => 'oauth_scopes:',
+            'public_key_key'  => 'oauth_public_keys:',
+        ), $config);
+    }
+
+    protected function getValue($key)
+    {
+        if (isset($this->cache[$key])) {
+            return $this->cache[$key];
+        }
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        try {
+            $value = $cf->get($key, new ColumnSlice("", ""));
+            $value = array_shift($value);
+        } catch (\cassandra\NotFoundException $e) {
+            return false;
+        }
+
+        return json_decode($value, true);
+    }
+
+    protected function setValue($key, $value, $expire = 0)
+    {
+        $this->cache[$key] = $value;
+
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        $str = json_encode($value);
+        if ($expire > 0) {
+            try {
+                $seconds = $expire - time();
+                // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds
+                $cf->insert($key, array('__data' => $str), null, $seconds);
+            } catch (\Exception $e) {
+                return false;
+            }
+        } else {
+            try {
+                // __data key set as C* requires a field
+                $cf->insert($key, array('__data' => $str));
+            } catch (\Exception $e) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected function expireValue($key)
+    {
+        unset($this->cache[$key]);
+
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        if ($cf->get_count($key) > 0) {
+            try {
+                // __data key set as C* requires a field
+                $cf->remove($key, array('__data'));
+            } catch (\Exception $e) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        return $this->getValue($this->config['code_key'] . $code);
+    }
+
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        return $this->setValue(
+            $this->config['code_key'] . $authorization_code,
+            compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
+            $expires
+        );
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $key = $this->config['code_key'] . $code;
+        unset($this->cache[$key]);
+
+        return $this->expireValue($key);
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    public function getUser($username)
+    {
+        if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username,
+        ), $userInfo);
+    }
+
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        $password = $this->hashPassword($password);
+
+        return $this->setValue(
+            $this->config['user_key'] . $username,
+            compact('username', 'password', 'first_name', 'last_name')
+        );
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return isset($client['client_secret'])
+            && $client['client_secret'] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return empty($client['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        return $this->getValue($this->config['client_key'] . $client_id);
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        return $this->setValue(
+            $this->config['client_key'] . $client_id,
+            compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
+        );
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['refresh_token_key'] . $refresh_token,
+            compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        return $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        return $this->getValue($this->config['access_token_key'].$access_token);
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['access_token_key'].$access_token,
+            compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        return $this->expireValue($this->config['access_token_key'] . $access_token);
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+
+        $result = $this->getValue($this->config['scope_key'].'supported:global');
+
+        $supportedScope = explode(' ', (string) $result);
+
+        return (count(array_diff($scope, $supportedScope)) == 0);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
+            $result = $this->getValue($this->config['scope_key'].'default:global');
+        }
+
+        return $result;
+    }
+
+    public function setScope($scope, $client_id = null, $type = 'supported')
+    {
+        if (!in_array($type, array('default', 'supported'))) {
+            throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
+        }
+
+        if (is_null($client_id)) {
+            $key = $this->config['scope_key'].$type.':global';
+        } else {
+            $key = $this->config['scope_key'].$type.':'.$client_id;
+        }
+
+        return $this->setValue($key, $scope);
+    }
+
+    /*JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
+            return false;
+        }
+
+        if (isset($jwt['subject']) && $jwt['subject'] == $subject ) {
+            return $jwt['key'];
+        }
+
+        return null;
+    }
+
+    public function setClientKey($client_id, $key, $subject = null)
+    {
+        return $this->setValue($this->config['jwt_key'] . $client_id, array(
+            'key' => $key,
+            'subject' => $subject
+        ));
+    }
+
+    /*ScopeInterface */
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs cassandra implementation.
+        throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs cassandra implementation.
+        throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
+    }
+
+    /* PublicKeyInterface */
+    public function getPublicKey($client_id = '')
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['public_key'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['public_key'];
+        }
+    }
+
+    public function getPrivateKey($client_id = '')
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['private_key'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['private_key'];
+        }
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['encryption_algorithm'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        $userDetails = $this->getUserDetails($user_id);
+        if (!is_array($userDetails)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            if ($value == 'email_verified') {
+                $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
+            } else {
+                $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+            }
+        }
+
+        return $userClaims;
+    }
+
+}

+ 540 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/DynamoDB.php

@@ -0,0 +1,540 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use Aws\DynamoDb\DynamoDbClient;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+/**
+ * DynamoDB storage for all storage types
+ *
+ * To use, install "aws/aws-sdk-php" via composer
+ * <code>
+ *  composer require aws/aws-sdk-php:dev-master
+ * </code>
+ *
+ * Once this is done, instantiate the DynamoDB client
+ * <code>
+ *  $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION"));
+ * </code>
+ *
+ * Table :
+ *  - oauth_access_tokens (primary hash key : access_token)
+ *  - oauth_authorization_codes (primary hash key : authorization_code)
+ *  - oauth_clients (primary hash key : client_id)
+ *  - oauth_jwt (primary hash key : client_id, primary range key : subject)
+ *  - oauth_public_keys (primary hash key : client_id)
+ *  - oauth_refresh_tokens (primary hash key : refresh_token)
+ *  - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default)
+ *  - oauth_users (primary hash key : username)
+ *
+ * @author Frederic AUGUSTE <frederic.auguste at gmail dot com>
+ */
+class DynamoDB implements
+    AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $client;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if (!($connection instanceof DynamoDbClient)) {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
+            }
+            if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
+            }
+            $this->client = DynamoDbClient::factory(array(
+                'key' => $connection["key"],
+                'secret' => $connection["secret"],
+                'region' =>$connection["region"]
+            ));
+        } else {
+            $this->client = $connection;
+        }
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table'  => 'oauth_jwt',
+            'scope_table'  => 'oauth_scopes',
+            'public_key_table'  => 'oauth_public_keys',
+        ), $config);
+    }
+
+    /* OAuth2\Storage\ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+
+        return  $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+
+        if ($result->count()==0) {
+            return false ;
+        }
+
+        return empty($result["Item"]["client_secret"]);
+    }
+
+    /* OAuth2\Storage\ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $result = $this->dynamo2array($result);
+        foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) {
+            if (!array_key_exists ($val, $result)) {
+                $result[$val] = null;
+            }
+        }
+
+        return $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['client_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* OAuth2\Storage\AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['access_token_table'],
+            "Key" => array('access_token'   => array('S' => $access_token))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        if (array_key_exists ('expires', $token)) {
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['access_token_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['access_token_table'],
+            'Key' => $this->client->formatAttributes(array("access_token" => $access_token)),
+            'ReturnValues' => 'ALL_OLD',
+        ));
+
+        return null !== $result->get('Attributes');
+    }
+
+    /* OAuth2\Storage\AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['code_table'],
+            "Key" => array('authorization_code'   => array('S' => $code))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        if (!array_key_exists("id_token", $token )) {
+            $token['id_token'] = null;
+        }
+        $token['expires'] = strtotime($token['expires']);
+
+        return $token;
+
+    }
+
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['code_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['code_table'],
+            'Key' => $this->client->formatAttributes(array("authorization_code" => $code))
+        ));
+
+        return true;
+    }
+
+    /* OAuth2\Storage\UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            if ($value == 'email_verified') {
+                $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
+            } else {
+                $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+            }
+        }
+
+        return $userClaims;
+    }
+
+    /* OAuth2\Storage\RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['refresh_token_table'],
+            "Key" => array('refresh_token'   => array('S' => $refresh_token))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        $token['expires'] = strtotime($token['expires']);
+
+        return $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['refresh_token_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['refresh_token_table'],
+            'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token))
+        ));
+
+        return true;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['user_table'],
+            "Key" => array('username'   => array('S' => $username))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        $token['user_id'] = $username;
+
+        return $token;
+    }
+
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        // do not store in plaintext
+        $password = $this->hashPassword($password);
+
+        $clientData = compact('username', 'password', 'first_name', 'last_name');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['user_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+        $scope_query = array();
+        $count = 0;
+        foreach ($scope as $key => $val) {
+            $result = $this->client->query(array(
+                'TableName'     => $this->config['scope_table'],
+                'Select'        => 'COUNT',
+                'KeyConditions' => array(
+                    'scope' => array(
+                        'AttributeValueList' => array(array('S' => $val)),
+                        'ComparisonOperator' => 'EQ'
+                    )
+                )
+            ));
+            $count += $result['Count'];
+        }
+
+        return $count == count($scope);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+
+        $result = $this->client->query(array(
+            'TableName' => $this->config['scope_table'],
+            'IndexName' => 'is_default-index',
+            'Select' => 'ALL_ATTRIBUTES',
+            'KeyConditions' => array(
+                'is_default' => array(
+                    'AttributeValueList' => array(array('S' => 'true')),
+                    'ComparisonOperator' => 'EQ',
+                ),
+            )
+        ));
+        $defaultScope = array();
+        if ($result->count() > 0) {
+            $array = $result->toArray();
+            foreach ($array["Items"] as $item) {
+                $defaultScope[]  = $item['scope']['S'];
+            }
+
+            return empty($defaultScope) ? null : implode(' ', $defaultScope);
+        }
+
+        return null;
+    }
+
+    /* JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['jwt_table'],
+            "Key" => array('client_id'   => array('S' => $client_id), 'subject' => array('S' => $subject))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['public_key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO not use.
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO not use.
+    }
+
+    /* PublicKeyInterface */
+    public function getPublicKey($client_id = '0')
+    {
+
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['public_key'];
+
+    }
+
+    public function getPrivateKey($client_id = '0')
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return 'RS256' ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['encryption_algorithm'];
+    }
+
+    /**
+     * Transform dynamodb resultset to an array.
+     * @param $dynamodbResult
+     * @return $array
+     */
+    private function dynamo2array($dynamodbResult)
+    {
+        $result = array();
+        foreach ($dynamodbResult["Item"] as $key => $val) {
+            $result[$key] = $val["S"];
+            $result[] = $val["S"];
+        }
+
+        return $result;
+    }
+
+    private static function isNotEmpty($value)
+    {
+        return null !== $value && '' !== $value;
+    }
+}

+ 381 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Memory.php

@@ -0,0 +1,381 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple in-memory storage for all storage types
+ *
+ * NOTE: This class should never be used in production, and is
+ * a stub class for example use only
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class Memory implements AuthorizationCodeInterface,
+    UserCredentialsInterface,
+    UserClaimsInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    public $authorizationCodes;
+    public $userCredentials;
+    public $clientCredentials;
+    public $refreshTokens;
+    public $accessTokens;
+    public $jwt;
+    public $jti;
+    public $supportedScopes;
+    public $defaultScope;
+    public $keys;
+
+    public function __construct($params = array())
+    {
+        $params = array_merge(array(
+            'authorization_codes' => array(),
+            'user_credentials' => array(),
+            'client_credentials' => array(),
+            'refresh_tokens' => array(),
+            'access_tokens' => array(),
+            'jwt' => array(),
+            'jti' => array(),
+            'default_scope' => null,
+            'supported_scopes' => array(),
+            'keys' => array(),
+        ), $params);
+
+        $this->authorizationCodes = $params['authorization_codes'];
+        $this->userCredentials = $params['user_credentials'];
+        $this->clientCredentials = $params['client_credentials'];
+        $this->refreshTokens = $params['refresh_tokens'];
+        $this->accessTokens = $params['access_tokens'];
+        $this->jwt = $params['jwt'];
+        $this->jti = $params['jti'];
+        $this->supportedScopes = $params['supported_scopes'];
+        $this->defaultScope = $params['default_scope'];
+        $this->keys = $params['keys'];
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        if (!isset($this->authorizationCodes[$code])) {
+            return false;
+        }
+
+        return array_merge(array(
+            'authorization_code' => $code,
+        ), $this->authorizationCodes[$code]);
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token');
+
+        return true;
+    }
+
+    public function setAuthorizationCodes($authorization_codes)
+    {
+        $this->authorizationCodes = $authorization_codes;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        unset($this->authorizationCodes[$code]);
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        $userDetails = $this->getUserDetails($username);
+
+        return $userDetails && $userDetails['password'] && $userDetails['password'] === $password;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        $this->userCredentials[$username] = array(
+            'password'   => $password,
+            'first_name' => $firstName,
+            'last_name'  => $lastName,
+        );
+
+        return true;
+    }
+
+    public function getUserDetails($username)
+    {
+        if (!isset($this->userCredentials[$username])) {
+            return false;
+        }
+
+        return array_merge(array(
+            'user_id'    => $username,
+            'password'   => null,
+            'first_name' => null,
+            'last_name'  => null,
+        ), $this->userCredentials[$username]);
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+        }
+
+        return $userClaims;
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!isset($this->clientCredentials[$client_id])) {
+            return false;
+        }
+
+        return empty($this->clientCredentials[$client_id]['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        if (!isset($this->clientCredentials[$client_id])) {
+            return false;
+        }
+
+        $clientDetails = array_merge(array(
+            'client_id'     => $client_id,
+            'client_secret' => null,
+            'redirect_uri'  => null,
+            'scope'         => null,
+        ), $this->clientCredentials[$client_id]);
+
+        return $clientDetails;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        if (isset($this->clientCredentials[$client_id]['grant_types'])) {
+            $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']);
+
+            return in_array($grant_type, $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        $this->clientCredentials[$client_id] = array(
+            'client_id'     => $client_id,
+            'client_secret' => $client_secret,
+            'redirect_uri'  => $redirect_uri,
+            'grant_types'   => $grant_types,
+            'scope'         => $scope,
+            'user_id'       => $user_id,
+        );
+
+        return true;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        if (isset($this->refreshTokens[$refresh_token])) {
+            unset($this->refreshTokens[$refresh_token]);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function setRefreshTokens($refresh_tokens)
+    {
+        $this->refreshTokens = $refresh_tokens;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null)
+    {
+        $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token');
+
+        return true;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        if (isset($this->accessTokens[$access_token])) {
+            unset($this->accessTokens[$access_token]);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', trim($scope));
+
+        return (count(array_diff($scope, $this->supportedScopes)) == 0);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        return $this->defaultScope;
+    }
+
+    /*JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        if (isset($this->jwt[$client_id])) {
+            $jwt = $this->jwt[$client_id];
+            if ($jwt) {
+                if ($jwt["subject"] == $subject) {
+                    return $jwt["key"];
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        foreach ($this->jti as $storedJti) {
+            if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) {
+                return array(
+                    'issuer' => $storedJti['issuer'],
+                    'subject' => $storedJti['subject'],
+                    'audience' => $storedJti['audience'],
+                    'expires' => $storedJti['expires'],
+                    'jti' => $storedJti['jti']
+                );
+            }
+        }
+
+        return null;
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti);
+    }
+
+    /*PublicKeyInterface */
+    public function getPublicKey($client_id = null)
+    {
+        if (isset($this->keys[$client_id])) {
+            return $this->keys[$client_id]['public_key'];
+        }
+
+        // use a global encryption pair
+        if (isset($this->keys['public_key'])) {
+            return $this->keys['public_key'];
+        }
+
+        return false;
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if (isset($this->keys[$client_id])) {
+            return $this->keys[$client_id]['private_key'];
+        }
+
+        // use a global encryption pair
+        if (isset($this->keys['private_key'])) {
+            return $this->keys['private_key'];
+        }
+
+        return false;
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if (isset($this->keys[$client_id]['encryption_algorithm'])) {
+            return $this->keys[$client_id]['encryption_algorithm'];
+        }
+
+        // use a global encryption algorithm
+        if (isset($this->keys['encryption_algorithm'])) {
+            return $this->keys['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+}

+ 392 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Mongo.php

@@ -0,0 +1,392 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple MongoDB storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Julien Chaumond <chaumond@gmail.com>
+ */
+class Mongo implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if ($connection instanceof \MongoDB) {
+            $this->db = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array');
+            }
+            $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']);
+            $m = new \MongoClient($server);
+            $this->db = $m->{$connection['database']};
+        }
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'key_table' => 'oauth_keys',
+            'jwt_table' => 'oauth_jwt',
+        ), $config);
+    }
+
+    // Helper function to access a MongoDB collection by `type`:
+    protected function collection($name)
+    {
+        return $this->db->{$this->config[$name]};
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return $result['client_secret'] == $client_secret;
+        }
+
+        return false;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return false;
+        }
+
+        return empty($result['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->collection('client_table')->findOne(array('client_id' => $client_id));
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        if ($this->getClientDetails($client_id)) {
+            $this->collection('client_table')->update(
+                array('client_id' => $client_id),
+                array('$set' => array(
+                    'client_secret' => $client_secret,
+                    'redirect_uri'  => $redirect_uri,
+                    'grant_types'   => $grant_types,
+                    'scope'         => $scope,
+                    'user_id'       => $user_id,
+                ))
+            );
+        } else {
+            $client = array(
+                'client_id'     => $client_id,
+                'client_secret' => $client_secret,
+                'redirect_uri'  => $redirect_uri,
+                'grant_types'   => $grant_types,
+                'scope'         => $scope,
+                'user_id'       => $user_id,
+            );
+            $this->collection('client_table')->insert($client);
+        }
+
+        return true;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token));
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $this->collection('access_token_table')->update(
+                array('access_token' => $access_token),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'expires' => $expires,
+                    'user_id' => $user_id,
+                    'scope' => $scope
+                ))
+            );
+        } else {
+            $token = array(
+                'access_token' => $access_token,
+                'client_id' => $client_id,
+                'expires' => $expires,
+                'user_id' => $user_id,
+                'scope' => $scope
+            );
+            $this->collection('access_token_table')->insert($token);
+        }
+
+        return true;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->collection('access_token_table')->remove(array(
+            'access_token' => $access_token
+        ), array('w' => 1));
+
+        return $result['n'] > 0;
+    }
+
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $code = $this->collection('code_table')->findOne(array('authorization_code' => $code));
+
+        return is_null($code) ? false : $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $this->collection('code_table')->update(
+                array('authorization_code' => $code),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'user_id' => $user_id,
+                    'redirect_uri' => $redirect_uri,
+                    'expires' => $expires,
+                    'scope' => $scope,
+                    'id_token' => $id_token,
+                ))
+            );
+        } else {
+            $token = array(
+                'authorization_code' => $code,
+                'client_id' => $client_id,
+                'user_id' => $user_id,
+                'redirect_uri' => $redirect_uri,
+                'expires' => $expires,
+                'scope' => $scope,
+                'id_token' => $id_token,
+            );
+            $this->collection('code_table')->insert($token);
+        }
+
+        return true;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $this->collection('code_table')->remove(array('authorization_code' => $code));
+
+        return true;
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        if ($user = $this->getUser($username)) {
+            $user['user_id'] = $user['username'];
+        }
+
+        return $user;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token));
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $token = array(
+            'refresh_token' => $refresh_token,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'expires' => $expires,
+            'scope' => $scope
+        );
+        $this->collection('refresh_token_table')->insert($token);
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->collection('refresh_token_table')->remove(array(
+            'refresh_token' => $refresh_token
+        ), array('w' => 1));
+
+        return $result['n'] > 0;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $password;
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->collection('user_table')->findOne(array('username' => $username));
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        if ($this->getUser($username)) {
+            $this->collection('user_table')->update(
+                array('username' => $username),
+                array('$set' => array(
+                    'password' => $password,
+                    'first_name' => $firstName,
+                    'last_name' => $lastName
+                ))
+            );
+        } else {
+            $user = array(
+                'username' => $username,
+                'password' => $password,
+                'first_name' => $firstName,
+                'last_name' => $lastName
+            );
+            $this->collection('user_table')->insert($user);
+        }
+
+        return true;
+    }
+
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->collection('jwt_table')->findOne(array(
+            'client_id' => $client_id,
+            'subject' => $subject
+        ));
+
+        return is_null($result) ? false : $result['key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function getPublicKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['public_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['public_key'];
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['private_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['encryption_algorithm'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? 'RS256' : $result['encryption_algorithm'];
+    }
+}

+ 380 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/MongoDB.php

@@ -0,0 +1,380 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use MongoDB\Client;
+use MongoDB\Database;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple MongoDB storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Julien Chaumond <chaumond@gmail.com>
+ */
+class MongoDB implements AuthorizationCodeInterface,
+    UserCredentialsInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if ($connection instanceof Database) {
+            $this->db = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array');
+            }
+            $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']);
+            $m = new Client($server);
+            $this->db = $m->selectDatabase($connection['database']);
+        }
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table' => 'oauth_jwt',
+            'jti_table' => 'oauth_jti',
+            'scope_table'  => 'oauth_scopes',
+            'key_table'  => 'oauth_keys',
+        ), $config);
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return $result['client_secret'] == $client_secret;
+        }
+        return false;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return false;
+        }
+        return empty($result['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->collection('client_table')->findOne(array('client_id' => $client_id));
+        return is_null($result) ? false : $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        if ($this->getClientDetails($client_id)) {
+            $result = $this->collection('client_table')->updateOne(
+                array('client_id' => $client_id),
+                array('$set' => array(
+                    'client_secret' => $client_secret,
+                    'redirect_uri'  => $redirect_uri,
+                    'grant_types'   => $grant_types,
+                    'scope'         => $scope,
+                    'user_id'       => $user_id,
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $client = array(
+            'client_id'     => $client_id,
+            'client_secret' => $client_secret,
+            'redirect_uri'  => $redirect_uri,
+            'grant_types'   => $grant_types,
+            'scope'         => $scope,
+            'user_id'       => $user_id,
+        );
+        $result = $this->collection('client_table')->insertOne($client);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+            return in_array($grant_type, $grant_types);
+        }
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token));
+        return is_null($token) ? false : $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $result = $this->collection('access_token_table')->updateOne(
+                array('access_token' => $access_token),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'expires' => $expires,
+                    'user_id' => $user_id,
+                    'scope' => $scope
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $token = array(
+            'access_token' => $access_token,
+            'client_id' => $client_id,
+            'expires' => $expires,
+            'user_id' => $user_id,
+            'scope' => $scope
+        );
+        $result = $this->collection('access_token_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->collection('access_token_table')->deleteOne(array(
+            'access_token' => $access_token
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $code = $this->collection('code_table')->findOne(array(
+            'authorization_code' => $code
+        ));
+        return is_null($code) ? false : $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $result = $this->collection('code_table')->updateOne(
+                array('authorization_code' => $code),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'user_id' => $user_id,
+                    'redirect_uri' => $redirect_uri,
+                    'expires' => $expires,
+                    'scope' => $scope,
+                    'id_token' => $id_token,
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $token = array(
+            'authorization_code' => $code,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'redirect_uri' => $redirect_uri,
+            'expires' => $expires,
+            'scope' => $scope,
+            'id_token' => $id_token,
+        );
+        $result = $this->collection('code_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $result = $this->collection('code_table')->deleteOne(array(
+            'authorization_code' => $code
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        if ($user = $this->getUser($username)) {
+            $user['user_id'] = $user['username'];
+        }
+        return $user;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $token = $this->collection('refresh_token_table')->findOne(array(
+            'refresh_token' => $refresh_token
+        ));
+        return is_null($token) ? false : $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $token = array(
+            'refresh_token' => $refresh_token,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'expires' => $expires,
+            'scope' => $scope
+        );
+        $result = $this->collection('refresh_token_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->collection('refresh_token_table')->deleteOne(array(
+            'refresh_token' => $refresh_token
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $password;
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->collection('user_table')->findOne(array('username' => $username));
+        return is_null($result) ? false : $result;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        if ($this->getUser($username)) {
+            $result = $this->collection('user_table')->updateOne(
+                array('username' => $username),
+                array('$set' => array(
+                    'password' => $password,
+                    'first_name' => $firstName,
+                    'last_name' => $lastName
+                ))
+            );
+
+            return $result->getMatchedCount() > 0;
+        }
+
+        $user = array(
+            'username' => $username,
+            'password' => $password,
+            'first_name' => $firstName,
+            'last_name' => $lastName
+        );
+        $result = $this->collection('user_table')->insertOne($user);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->collection('jwt_table')->findOne(array(
+            'client_id' => $client_id,
+            'subject' => $subject
+        ));
+        return is_null($result) ? false : $result['key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function getPublicKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['public_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['public_key'];
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['private_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['encryption_algorithm'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? 'RS256' : $result['encryption_algorithm'];
+    }
+
+    // Helper function to access a MongoDB collection by `type`:
+    protected function collection($name)
+    {
+        return $this->db->{$this->config[$name]};
+    }
+}

+ 553 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Pdo.php

@@ -0,0 +1,553 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple PDO storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class Pdo implements
+    AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if (!$connection instanceof \PDO) {
+            if (is_string($connection)) {
+                $connection = array('dsn' => $connection);
+            }
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array');
+            }
+            if (!isset($connection['dsn'])) {
+                throw new \InvalidArgumentException('configuration array must contain "dsn"');
+            }
+            // merge optional parameters
+            $connection = array_merge(array(
+                'username' => null,
+                'password' => null,
+                'options' => array(),
+            ), $connection);
+            $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password'], $connection['options']);
+        }
+        $this->db = $connection;
+
+        // debugging
+        $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table'  => 'oauth_jwt',
+            'jti_table'  => 'oauth_jti',
+            'scope_table'  => 'oauth_scopes',
+            'public_key_table'  => 'oauth_public_keys',
+        ), $config);
+    }
+
+    /* OAuth2\Storage\ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        // make this extensible
+        return $result && $result['client_secret'] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+
+        if (!$result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return false;
+        }
+
+        return empty($result['client_secret']);
+    }
+
+    /* OAuth2\Storage\ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+
+        return $stmt->fetch(\PDO::FETCH_ASSOC);
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        // if it exists, update it.
+        if ($this->getClientDetails($client_id)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table']));
+        }
+
+        return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'));
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* OAuth2\Storage\AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table']));
+
+        $token = $stmt->execute(compact('access_token'));
+        if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert date string back to timestamp
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $stmt = $this->db->prepare(sprintf('UPDATE %s SET client_id=:client_id, expires=:expires, user_id=:user_id, scope=:scope where access_token=:access_token', $this->config['access_token_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (access_token, client_id, expires, user_id, scope) VALUES (:access_token, :client_id, :expires, :user_id, :scope)', $this->config['access_token_table']));
+        }
+
+        return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope'));
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table']));
+
+        $stmt->execute(compact('access_token'));
+
+        return $stmt->rowCount() > 0;
+    }
+
+    /* OAuth2\Storage\AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table']));
+        $stmt->execute(compact('code'));
+
+        if ($code = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert date string back to timestamp
+            $code['expires'] = strtotime($code['expires']);
+        }
+
+        return $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        if (func_num_args() > 6) {
+            // we are calling with an id token
+            return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
+        }
+
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table']));
+        }
+
+        return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'));
+    }
+
+    private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table']));
+        }
+
+        return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'));
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table']));
+
+        return $stmt->execute(compact('code'));
+    }
+
+    /* OAuth2\Storage\UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+        }
+
+        return $userClaims;
+    }
+
+    /* OAuth2\Storage\RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table']));
+
+        $token = $stmt->execute(compact('refresh_token'));
+        if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert expires to epoch time
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $stmt = $this->db->prepare(sprintf('INSERT INTO %s (refresh_token, client_id, user_id, expires, scope) VALUES (:refresh_token, :client_id, :user_id, :expires, :scope)', $this->config['refresh_token_table']));
+
+        return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'));
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table']));
+
+        $stmt->execute(compact('refresh_token'));
+
+        return $stmt->rowCount() > 0;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    public function getUser($username)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table']));
+        $stmt->execute(array('username' => $username));
+
+        if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username
+        ), $userInfo);
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        // do not store in plaintext
+        $password = $this->hashPassword($password);
+
+        // if it exists, update it.
+        if ($this->getUser($username)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table']));
+        }
+
+        return $stmt->execute(compact('username', 'password', 'firstName', 'lastName'));
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+        $whereIn = implode(',', array_fill(0, count($scope), '?'));
+        $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn));
+        $stmt->execute($scope);
+
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['count'] == count($scope);
+        }
+
+        return false;
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table']));
+        $stmt->execute(array('is_default' => true));
+
+        if ($result = $stmt->fetchAll(\PDO::FETCH_ASSOC)) {
+            $defaultScope = array_map(function ($row) {
+                return $row['scope'];
+            }, $result);
+
+            return implode(' ', $defaultScope);
+        }
+
+        return null;
+    }
+
+    /* JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table']));
+
+        $stmt->execute(array('client_id' => $client_id, 'subject' => $subject));
+
+        return $stmt->fetchColumn();
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table']));
+
+        $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti'));
+
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return array(
+                'issuer' => $result['issuer'],
+                'subject' => $result['subject'],
+                'audience' => $result['audience'],
+                'expires' => $result['expires'],
+                'jti' => $result['jti'],
+            );
+        }
+
+        return null;
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table']));
+
+        return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti'));
+    }
+
+    /* PublicKeyInterface */
+    public function getPublicKey($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['public_key'];
+        }
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['private_key'];
+        }
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+
+    /**
+     * DDL to create OAuth2 database and tables for PDO storage
+     *
+     * @see https://github.com/dsquier/oauth2-server-php-mysql
+     */
+    public function getBuildSql($dbName = 'oauth2_server_php')
+    {
+        $sql = "
+        CREATE TABLE {$this->config['client_table']} (
+          client_id             VARCHAR(80)   NOT NULL,
+          client_secret         VARCHAR(80),
+          redirect_uri          VARCHAR(2000),
+          grant_types           VARCHAR(80),
+          scope                 VARCHAR(4000),
+          user_id               VARCHAR(80),
+          PRIMARY KEY (client_id)
+        );
+
+        CREATE TABLE {$this->config['access_token_table']} (
+          access_token         VARCHAR(40)    NOT NULL,
+          client_id            VARCHAR(80)    NOT NULL,
+          user_id              VARCHAR(80),
+          expires              TIMESTAMP      NOT NULL,
+          scope                VARCHAR(4000),
+          PRIMARY KEY (access_token)
+        );
+
+        CREATE TABLE {$this->config['code_table']} (
+          authorization_code  VARCHAR(40)    NOT NULL,
+          client_id           VARCHAR(80)    NOT NULL,
+          user_id             VARCHAR(80),
+          redirect_uri        VARCHAR(2000),
+          expires             TIMESTAMP      NOT NULL,
+          scope               VARCHAR(4000),
+          id_token            VARCHAR(1000),
+          PRIMARY KEY (authorization_code)
+        );
+
+        CREATE TABLE {$this->config['refresh_token_table']} (
+          refresh_token       VARCHAR(40)    NOT NULL,
+          client_id           VARCHAR(80)    NOT NULL,
+          user_id             VARCHAR(80),
+          expires             TIMESTAMP      NOT NULL,
+          scope               VARCHAR(4000),
+          PRIMARY KEY (refresh_token)
+        );
+
+        CREATE TABLE {$this->config['user_table']} (
+          username            VARCHAR(80),
+          password            VARCHAR(80),
+          first_name          VARCHAR(80),
+          last_name           VARCHAR(80),
+          email               VARCHAR(80),
+          email_verified      BOOLEAN,
+          scope               VARCHAR(4000)
+        );
+
+        CREATE TABLE {$this->config['scope_table']} (
+          scope               VARCHAR(80)  NOT NULL,
+          is_default          BOOLEAN,
+          PRIMARY KEY (scope)
+        );
+
+        CREATE TABLE {$this->config['jwt_table']} (
+          client_id           VARCHAR(80)   NOT NULL,
+          subject             VARCHAR(80),
+          public_key          VARCHAR(2000) NOT NULL
+        );
+
+        CREATE TABLE {$this->config['jti_table']} (
+          issuer              VARCHAR(80)   NOT NULL,
+          subject             VARCHAR(80),
+          audience            VARCHAR(80),
+          expires             TIMESTAMP     NOT NULL,
+          jti                 VARCHAR(2000) NOT NULL
+        );
+
+        CREATE TABLE {$this->config['public_key_table']} (
+          client_id            VARCHAR(80),
+          public_key           VARCHAR(2000),
+          private_key          VARCHAR(2000),
+          encryption_algorithm VARCHAR(100) DEFAULT 'RS256'
+        )
+";
+
+        return $sql;
+    }
+}

+ 321 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/Redis.php

@@ -0,0 +1,321 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * redis storage for all storage types
+ *
+ * To use, install "predis/predis" via composer
+ *
+ * Register client:
+ * <code>
+ *  $storage = new OAuth2\Storage\Redis($redis);
+ *  $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
+ * </code>
+ */
+class Redis implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    OpenIDAuthorizationCodeInterface
+{
+
+    private $cache;
+
+    /* The redis client */
+    protected $redis;
+
+    /* Configuration array */
+    protected $config;
+
+    /**
+     * Redis Storage!
+     *
+     * @param \Predis\Client $redis
+     * @param array          $config
+     */
+    public function __construct($redis, $config=array())
+    {
+        $this->redis = $redis;
+        $this->config = array_merge(array(
+            'client_key' => 'oauth_clients:',
+            'access_token_key' => 'oauth_access_tokens:',
+            'refresh_token_key' => 'oauth_refresh_tokens:',
+            'code_key' => 'oauth_authorization_codes:',
+            'user_key' => 'oauth_users:',
+            'jwt_key' => 'oauth_jwt:',
+            'scope_key' => 'oauth_scopes:',
+        ), $config);
+    }
+
+    protected function getValue($key)
+    {
+        if ( isset($this->cache[$key]) ) {
+            return $this->cache[$key];
+        }
+        $value = $this->redis->get($key);
+        if ( isset($value) ) {
+            return json_decode($value, true);
+        } else {
+            return false;
+        }
+    }
+
+    protected function setValue($key, $value, $expire=0)
+    {
+        $this->cache[$key] = $value;
+        $str = json_encode($value);
+        if ($expire > 0) {
+            $seconds = $expire - time();
+            $ret = $this->redis->setex($key, $seconds, $str);
+        } else {
+            $ret = $this->redis->set($key, $str);
+        }
+
+        // check that the key was set properly
+        // if this fails, an exception will usually thrown, so this step isn't strictly necessary
+        return is_bool($ret) ? $ret : $ret->getPayload() == 'OK';
+    }
+
+    protected function expireValue($key)
+    {
+        unset($this->cache[$key]);
+
+        return $this->redis->del($key);
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        return $this->getValue($this->config['code_key'] . $code);
+    }
+
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        return $this->setValue(
+            $this->config['code_key'] . $authorization_code,
+            compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
+            $expires
+        );
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $key = $this->config['code_key'] . $code;
+        unset($this->cache[$key]);
+
+        return $this->expireValue($key);
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        $user = $this->getUserDetails($username);
+
+        return $user && $user['password'] === $password;
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    public function getUser($username)
+    {
+        if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username,
+        ), $userInfo);
+    }
+
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        return $this->setValue(
+            $this->config['user_key'] . $username,
+            compact('username', 'password', 'first_name', 'last_name')
+        );
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return isset($client['client_secret'])
+            && $client['client_secret'] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return empty($client['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        return $this->getValue($this->config['client_key'] . $client_id);
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        return $this->setValue(
+            $this->config['client_key'] . $client_id,
+            compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
+        );
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['refresh_token_key'] . $refresh_token,
+            compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
+
+        return $result > 0;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        return $this->getValue($this->config['access_token_key'].$access_token);
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['access_token_key'].$access_token,
+            compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->expireValue($this->config['access_token_key'] . $access_token);
+
+        return $result > 0;
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+
+        $result = $this->getValue($this->config['scope_key'].'supported:global');
+
+        $supportedScope = explode(' ', (string) $result);
+
+        return (count(array_diff($scope, $supportedScope)) == 0);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
+            $result = $this->getValue($this->config['scope_key'].'default:global');
+        }
+
+        return $result;
+    }
+
+    public function setScope($scope, $client_id = null, $type = 'supported')
+    {
+        if (!in_array($type, array('default', 'supported'))) {
+            throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
+        }
+
+        if (is_null($client_id)) {
+            $key = $this->config['scope_key'].$type.':global';
+        } else {
+            $key = $this->config['scope_key'].$type.':'.$client_id;
+        }
+
+        return $this->setValue($key, $scope);
+    }
+
+    /*JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
+            return false;
+        }
+
+        if (isset($jwt['subject']) && $jwt['subject'] == $subject) {
+            return $jwt['key'];
+        }
+
+        return null;
+    }
+
+    public function setClientKey($client_id, $key, $subject = null)
+    {
+        return $this->setValue($this->config['jwt_key'] . $client_id, array(
+            'key' => $key,
+            'subject' => $subject
+        ));
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs redis implementation.
+        throw new \Exception('getJti() for the Redis driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs redis implementation.
+        throw new \Exception('setJti() for the Redis driver is currently unimplemented.');
+    }
+}

+ 82 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/OAuth2/Storage/RefreshTokenInterface.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save refresh tokens for the "Refresh Token"
+ * grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface RefreshTokenInterface
+{
+    /**
+     * Grant refresh access tokens.
+     *
+     * Retrieve the stored data for the given refresh token.
+     *
+     * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN.
+     *
+     * @param $refresh_token
+     * Refresh token to be check with.
+     *
+     * @return
+     * An associative array as below, and NULL if the refresh_token is
+     * invalid:
+     * - refresh_token: Refresh token identifier.
+     * - client_id: Client identifier.
+     * - user_id: User identifier.
+     * - expires: Expiration unix timestamp, or 0 if the token doesn't expire.
+     * - scope: (optional) Scope values in space-separated string.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-6
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function getRefreshToken($refresh_token);
+
+    /**
+     * Take the provided refresh token values and store them somewhere.
+     *
+     * This function should be the storage counterpart to getRefreshToken().
+     *
+     * If storage fails for some reason, we're not currently checking for
+     * any sort of success/failure, so you should bail out of the script
+     * and provide a descriptive fail message.
+     *
+     * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN.
+     *
+     * @param $refresh_token
+     * Refresh token to be stored.
+     * @param $client_id
+     * Client identifier to be stored.
+     * @param $user_id
+     * User identifier to be stored.
+     * @param $expires
+     * Expiration timestamp to be stored. 0 if the token doesn't expire.
+     * @param $scope
+     * (optional) Scopes to be stored in space-separated string.
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null);
+
+    /**
+     * Expire a used refresh token.
+     *
+     * This is not explicitly required in the spec, but is almost implied.
+     * After granting a new refresh token, the old one is no longer useful and
+     * so should be forcibly expired in the data store so it can't be used again.
+     *
+     * If storage fails for some reason, we're not currently checking for
+     * any sort of success/failure, so you should bail out of the script
+     * and provide a descriptive fail message.
+     *
+     * @param $refresh_token
+     * Refresh token to be expired.
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function unsetRefreshToken($refresh_token);
+}

+ 2390 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/amazon-s3/S3.php

@@ -0,0 +1,2390 @@
+<?php
+/**
+* $Id$
+*
+* Copyright (c) 2013, Donovan Schönknecht.  All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are met:
+*
+* - Redistributions of source code must retain the above copyright notice,
+*   this list of conditions and the following disclaimer.
+* - Redistributions in binary form must reproduce the above copyright
+*   notice, this list of conditions and the following disclaimer in the
+*   documentation and/or other materials provided with the distribution.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+* POSSIBILITY OF SUCH DAMAGE.
+*
+* Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
+*/
+
+/**
+* Amazon S3 PHP class
+*
+* @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
+* @version 0.5.1
+*/
+class S3
+{
+	// ACL flags
+	const ACL_PRIVATE = 'private';
+	const ACL_PUBLIC_READ = 'public-read';
+	const ACL_PUBLIC_READ_WRITE = 'public-read-write';
+	const ACL_AUTHENTICATED_READ = 'authenticated-read';
+
+	const STORAGE_CLASS_STANDARD = 'STANDARD';
+	const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY';
+	const STORAGE_CLASS_STANDARD_IA = 'STANDARD_IA';
+
+	const SSE_NONE = '';
+	const SSE_AES256 = 'AES256';
+
+	/**
+	 * The AWS Access key
+	 *
+	 * @var string
+	 * @access private
+	 * @static
+	 */
+	private static $__accessKey = null;
+
+	/**
+	 * AWS Secret Key
+	 *
+	 * @var string
+	 * @access private
+	 * @static
+	 */
+	private static $__secretKey = null;
+
+	/**
+	 * SSL Client key
+	 *
+	 * @var string
+	 * @access private
+	 * @static
+	 */
+	private static $__sslKey = null;
+
+	/**
+	 * Default delimiter to be used, for example while getBucket().
+	 * @var string
+	 * @access public
+	 * @static 
+	 */
+	public static $defDelimiter = null;
+
+	/**
+	 * AWS URI
+	 *
+	 * @var string
+	 * @acess public
+	 * @static
+	 */
+	public static $endpoint = 's3.amazonaws.com';
+
+	/**
+	 * Proxy information
+	 *
+	 * @var null|array
+	 * @access public
+	 * @static
+	 */
+	public static $proxy = null;
+
+	/**
+	 * Connect using SSL?
+	 *
+	 * @var bool
+	 * @access public
+	 * @static
+	 */
+	public static $useSSL = false;
+
+	/**
+	 * Use SSL validation?
+	 *
+	 * @var bool
+	 * @access public
+	 * @static
+	 */
+	public static $useSSLValidation = true;
+
+	/**
+	 * Use SSL version
+	 *
+	 * @var const
+	 * @access public
+	 * @static
+	 */
+	public static $useSSLVersion = CURL_SSLVERSION_TLSv1;
+
+	/**
+	 * Use PHP exceptions?
+	 *
+	 * @var bool
+	 * @access public
+	 * @static
+	 */
+	public static $useExceptions = false;
+
+	/**
+	 * Time offset applied to time()
+	 * @access private
+	 * @static
+	 */
+	private static $__timeOffset = 0;
+
+	/**
+	 * SSL client key
+	 *
+	 * @var bool
+	 * @access public
+	 * @static
+	 */
+	public static $sslKey = null;
+	
+	/**
+	 * SSL client certfificate
+	 *
+	 * @var string
+	 * @acess public
+	 * @static
+	 */
+	public static $sslCert = null;
+	
+	/**
+	 * SSL CA cert (only required if you are having problems with your system CA cert)
+	 *
+	 * @var string
+	 * @access public
+	 * @static
+	 */
+	public static $sslCACert = null;
+	
+	/**
+	 * AWS Key Pair ID
+	 *
+	 * @var string
+	 * @access private
+	 * @static
+	 */
+	private static $__signingKeyPairId = null;
+	
+	/**
+	 * Key resource, freeSigningKey() must be called to clear it from memory
+	 *
+	 * @var bool
+	 * @access private
+	 * @static 
+	 */
+	private static $__signingKeyResource = false;
+
+
+	/**
+	* Constructor - if you're not using the class statically
+	*
+	* @param string $accessKey Access key
+	* @param string $secretKey Secret key
+	* @param boolean $useSSL Enable SSL
+	* @param string $endpoint Amazon URI
+	* @return void
+	*/
+	public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com')
+	{
+		if ($accessKey !== null && $secretKey !== null)
+			self::setAuth($accessKey, $secretKey);
+		self::$useSSL = $useSSL;
+		self::$endpoint = $endpoint;
+	}
+
+
+	/**
+	* Set the service endpoint
+	*
+	* @param string $host Hostname
+	* @return void
+	*/
+	public function setEndpoint($host)
+	{
+		self::$endpoint = $host;
+	}
+
+
+	/**
+	* Set AWS access key and secret key
+	*
+	* @param string $accessKey Access key
+	* @param string $secretKey Secret key
+	* @return void
+	*/
+	public static function setAuth($accessKey, $secretKey)
+	{
+		self::$__accessKey = $accessKey;
+		self::$__secretKey = $secretKey;
+	}
+
+
+	/**
+	* Check if AWS keys have been set
+	*
+	* @return boolean
+	*/
+	public static function hasAuth() {
+		return (self::$__accessKey !== null && self::$__secretKey !== null);
+	}
+
+
+	/**
+	* Set SSL on or off
+	*
+	* @param boolean $enabled SSL enabled
+	* @param boolean $validate SSL certificate validation
+	* @return void
+	*/
+	public static function setSSL($enabled, $validate = true)
+	{
+		self::$useSSL = $enabled;
+		self::$useSSLValidation = $validate;
+	}
+
+
+	/**
+	* Set SSL client certificates (experimental)
+	*
+	* @param string $sslCert SSL client certificate
+	* @param string $sslKey SSL client key
+	* @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert)
+	* @return void
+	*/
+	public static function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null)
+	{
+		self::$sslCert = $sslCert;
+		self::$sslKey = $sslKey;
+		self::$sslCACert = $sslCACert;
+	}
+
+
+	/**
+	* Set proxy information
+	*
+	* @param string $host Proxy hostname and port (localhost:1234)
+	* @param string $user Proxy username
+	* @param string $pass Proxy password
+	* @param constant $type CURL proxy type
+	* @return void
+	*/
+	public static function setProxy($host, $user = null, $pass = null, $type = CURLPROXY_SOCKS5)
+	{
+		self::$proxy = array('host' => $host, 'type' => $type, 'user' => $user, 'pass' => $pass);
+	}
+
+
+	/**
+	* Set the error mode to exceptions
+	*
+	* @param boolean $enabled Enable exceptions
+	* @return void
+	*/
+	public static function setExceptions($enabled = true)
+	{
+		self::$useExceptions = $enabled;
+	}
+
+
+	/**
+	* Set AWS time correction offset (use carefully)
+	*
+	* This can be used when an inaccurate system time is generating
+	* invalid request signatures.  It should only be used as a last
+	* resort when the system time cannot be changed.
+	*
+	* @param string $offset Time offset (set to zero to use AWS server time)
+	* @return void
+	*/
+	public static function setTimeCorrectionOffset($offset = 0)
+	{
+		if ($offset == 0)
+		{
+			$rest = new S3Request('HEAD');
+			$rest = $rest->getResponse();
+			$awstime = $rest->headers['date'];
+			$systime = time();			
+			$offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime);
+		}
+		self::$__timeOffset = $offset;
+	}
+
+
+	/**
+	* Set signing key
+	*
+	* @param string $keyPairId AWS Key Pair ID
+	* @param string $signingKey Private Key
+	* @param boolean $isFile Load private key from file, set to false to load string
+	* @return boolean
+	*/
+	public static function setSigningKey($keyPairId, $signingKey, $isFile = true)
+	{
+		self::$__signingKeyPairId = $keyPairId;
+		if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ?
+		file_get_contents($signingKey) : $signingKey)) !== false) return true;
+		self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__);
+		return false;
+	}
+
+
+	/**
+	* Free signing key from memory, MUST be called if you are using setSigningKey()
+	*
+	* @return void
+	*/
+	public static function freeSigningKey()
+	{
+		if (self::$__signingKeyResource !== false)
+			openssl_free_key(self::$__signingKeyResource);
+	}
+
+
+	/**
+	* Internal error handler
+	*
+	* @internal Internal error handler
+	* @param string $message Error message
+	* @param string $file Filename
+	* @param integer $line Line number
+	* @param integer $code Error code
+	* @return void
+	*/
+	private static function __triggerError($message, $file, $line, $code = 0)
+	{
+		if (self::$useExceptions)
+			throw new S3Exception($message, $file, $line, $code);
+		else
+			trigger_error($message, E_USER_WARNING);
+	}
+
+
+	/**
+	* Get a list of buckets
+	*
+	* @param boolean $detailed Returns detailed bucket list when true
+	* @return array | false
+	*/
+	public static function listBuckets($detailed = false)
+	{
+		$rest = new S3Request('GET', '', '', self::$endpoint);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'],
+			$rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		$results = array();
+		if (!isset($rest->body->Buckets)) return $results;
+
+		if ($detailed)
+		{
+			if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
+			$results['owner'] = array(
+				'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
+			);
+			$results['buckets'] = array();
+			foreach ($rest->body->Buckets->Bucket as $b)
+				$results['buckets'][] = array(
+					'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate)
+				);
+		} else
+			foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name;
+
+		return $results;
+	}
+
+
+	/**
+	* Get contents for a bucket
+	*
+	* If maxKeys is null this method will loop through truncated result sets
+	*
+	* @param string $bucket Bucket name
+	* @param string $prefix Prefix
+	* @param string $marker Marker (last file listed)
+	* @param string $maxKeys Max keys (maximum number of keys to return)
+	* @param string $delimiter Delimiter
+	* @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes
+	* @return array | false
+	*/
+	public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false)
+	{
+		$rest = new S3Request('GET', $bucket, '', self::$endpoint);
+		if ($maxKeys == 0) $maxKeys = null;
+		if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
+		if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker);
+		if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys);
+		if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
+		else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter);
+		$response = $rest->getResponse();
+		if ($response->error === false && $response->code !== 200)
+			$response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status');
+		if ($response->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getBucket(): [%s] %s",
+			$response->error['code'], $response->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+
+		$results = array();
+
+		$nextMarker = null;
+		if (isset($response->body, $response->body->Contents))
+		foreach ($response->body->Contents as $c)
+		{
+			$results[(string)$c->Key] = array(
+				'name' => (string)$c->Key,
+				'time' => strtotime((string)$c->LastModified),
+				'size' => (int)$c->Size,
+				'hash' => substr((string)$c->ETag, 1, -1)
+			);
+			$nextMarker = (string)$c->Key;
+		}
+
+		if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
+			foreach ($response->body->CommonPrefixes as $c)
+				$results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
+
+		if (isset($response->body, $response->body->IsTruncated) &&
+		(string)$response->body->IsTruncated == 'false') return $results;
+
+		if (isset($response->body, $response->body->NextMarker))
+			$nextMarker = (string)$response->body->NextMarker;
+
+		// Loop through truncated results if maxKeys isn't specified
+		if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true')
+		do
+		{
+			$rest = new S3Request('GET', $bucket, '', self::$endpoint);
+			if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix);
+			$rest->setParameter('marker', $nextMarker);
+			if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter);
+
+			if (($response = $rest->getResponse()) == false || $response->code !== 200) break;
+
+			if (isset($response->body, $response->body->Contents))
+			foreach ($response->body->Contents as $c)
+			{
+				$results[(string)$c->Key] = array(
+					'name' => (string)$c->Key,
+					'time' => strtotime((string)$c->LastModified),
+					'size' => (int)$c->Size,
+					'hash' => substr((string)$c->ETag, 1, -1)
+				);
+				$nextMarker = (string)$c->Key;
+			}
+
+			if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes))
+				foreach ($response->body->CommonPrefixes as $c)
+					$results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix);
+
+			if (isset($response->body, $response->body->NextMarker))
+				$nextMarker = (string)$response->body->NextMarker;
+
+		} while ($response !== false && (string)$response->body->IsTruncated == 'true');
+
+		return $results;
+	}
+
+
+	/**
+	* Put a bucket
+	*
+	* @param string $bucket Bucket name
+	* @param constant $acl ACL flag
+	* @param string $location Set as "EU" to create buckets hosted in Europe
+	* @return boolean
+	*/
+	public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false)
+	{
+		$rest = new S3Request('PUT', $bucket, '', self::$endpoint);
+		$rest->setAmzHeader('x-amz-acl', $acl);
+
+		if ($location !== false)
+		{
+			$dom = new DOMDocument;
+			$createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
+			$locationConstraint = $dom->createElement('LocationConstraint', $location);
+			$createBucketConfiguration->appendChild($locationConstraint);
+			$dom->appendChild($createBucketConfiguration);
+			$rest->data = $dom->saveXML();
+			$rest->size = strlen($rest->data);
+			$rest->setHeader('Content-Type', 'application/xml');
+		}
+		$rest = $rest->getResponse();
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Delete an empty bucket
+	*
+	* @param string $bucket Bucket name
+	* @return boolean
+	*/
+	public static function deleteBucket($bucket)
+	{
+		$rest = new S3Request('DELETE', $bucket, '', self::$endpoint);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 204)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Create input info array for putObject()
+	*
+	* @param string $file Input file
+	* @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
+	* @return array | false
+	*/
+	public static function inputFile($file, $md5sum = true)
+	{
+		if (!file_exists($file) || !is_file($file) || !is_readable($file))
+		{
+			self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__);
+			return false;
+		}
+		clearstatcache(false, $file);
+		return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
+		(is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '');
+	}
+
+
+	/**
+	* Create input array info for putObject() with a resource
+	*
+	* @param string $resource Input resource to read from
+	* @param integer $bufferSize Input byte size
+	* @param string $md5sum MD5 hash to send (optional)
+	* @return array | false
+	*/
+	public static function inputResource(&$resource, $bufferSize = false, $md5sum = '')
+	{
+		if (!is_resource($resource) || (int)$bufferSize < 0)
+		{
+			self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
+			return false;
+		}
+
+		// Try to figure out the bytesize
+		if ($bufferSize === false)
+		{
+			if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false)
+			{
+				self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__);
+				return false;
+			}
+			fseek($resource, 0);
+		}
+
+		$input = array('size' => $bufferSize, 'md5sum' => $md5sum);
+		$input['fp'] =& $resource;
+		return $input;
+	}
+
+
+	/**
+	* Put an object
+	*
+	* @param mixed $input Input data
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param constant $acl ACL constant
+	* @param array $metaHeaders Array of x-amz-meta-* headers
+	* @param array $requestHeaders Array of request headers or content type as a string
+	* @param constant $storageClass Storage class constant
+	* @param constant $serverSideEncryption Server-side encryption
+	* @return boolean
+	*/
+	public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE)
+	{
+		if ($input === false) return false;
+		$rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
+
+		if (!is_array($input)) $input = array(
+			'data' => $input, 'size' => strlen($input),
+			'md5sum' => base64_encode(md5($input, true))
+		);
+
+		// Data
+		if (isset($input['fp']))
+			$rest->fp =& $input['fp'];
+		elseif (isset($input['file']))
+			$rest->fp = @fopen($input['file'], 'rb');
+		elseif (isset($input['data']))
+			$rest->data = $input['data'];
+
+		// Content-Length (required)
+		if (isset($input['size']) && $input['size'] >= 0)
+			$rest->size = $input['size'];
+		else {
+			if (isset($input['file'])) {
+				clearstatcache(false, $input['file']);
+				$rest->size = filesize($input['file']);
+			}
+			elseif (isset($input['data']))
+				$rest->size = strlen($input['data']);
+		}
+
+		// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
+		if (is_array($requestHeaders))
+			foreach ($requestHeaders as $h => $v)
+				strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
+		elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
+			$input['type'] = $requestHeaders;
+
+		// Content-Type
+		if (!isset($input['type']))
+		{
+			if (isset($requestHeaders['Content-Type']))
+				$input['type'] =& $requestHeaders['Content-Type'];
+			elseif (isset($input['file']))
+				$input['type'] = self::__getMIMEType($input['file']);
+			else
+				$input['type'] = 'application/octet-stream';
+		}
+
+		if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
+			$rest->setAmzHeader('x-amz-storage-class', $storageClass);
+
+		if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption
+			$rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption);
+
+		// We need to post with Content-Length and Content-Type, MD5 is optional
+		if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false))
+		{
+			$rest->setHeader('Content-Type', $input['type']);
+			if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
+
+			$rest->setAmzHeader('x-amz-acl', $acl);
+			foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
+			$rest->getResponse();
+		} else
+			$rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
+
+		if ($rest->response->error === false && $rest->response->code !== 200)
+			$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->response->error !== false)
+		{
+			self::__triggerError(sprintf("S3::putObject(): [%s] %s",
+			$rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Put an object from a file (legacy function)
+	*
+	* @param string $file Input file path
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param constant $acl ACL constant
+	* @param array $metaHeaders Array of x-amz-meta-* headers
+	* @param string $contentType Content type
+	* @return boolean
+	*/
+	public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null)
+	{
+		return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
+	}
+
+
+	/**
+	* Put an object from a string (legacy function)
+	*
+	* @param string $string Input data
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param constant $acl ACL constant
+	* @param array $metaHeaders Array of x-amz-meta-* headers
+	* @param string $contentType Content type
+	* @return boolean
+	*/
+	public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain')
+	{
+		return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
+	}
+
+
+	/**
+	* Get an object
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param mixed $saveTo Filename or resource to write to
+	* @return mixed
+	*/
+	public static function getObject($bucket, $uri, $saveTo = false)
+	{
+		$rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
+		if ($saveTo !== false)
+		{
+			if (is_resource($saveTo))
+				$rest->fp =& $saveTo;
+			else
+				if (($rest->fp = @fopen($saveTo, 'wb')) !== false)
+					$rest->file = realpath($saveTo);
+				else
+					$rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
+		}
+		if ($rest->response->error === false) $rest->getResponse();
+
+		if ($rest->response->error === false && $rest->response->code !== 200)
+			$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->response->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s",
+			$rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return $rest->response;
+	}
+
+
+	/**
+	* Get object information
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param boolean $returnInfo Return response information
+	* @return mixed | false
+	*/
+	public static function getObjectInfo($bucket, $uri, $returnInfo = true)
+	{
+		$rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404))
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
+	}
+
+
+	/**
+	* Copy an object
+	*
+	* @param string $srcBucket Source bucket name
+	* @param string $srcUri Source object URI
+	* @param string $bucket Destination bucket name
+	* @param string $uri Destination object URI
+	* @param constant $acl ACL constant
+	* @param array $metaHeaders Optional array of x-amz-meta-* headers
+	* @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
+	* @param constant $storageClass Storage class constant
+	* @return mixed | false
+	*/
+	public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD)
+	{
+		$rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
+		$rest->setHeader('Content-Length', 0);
+		foreach ($requestHeaders as $h => $v)
+				strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
+		foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
+		if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
+			$rest->setAmzHeader('x-amz-storage-class', $storageClass);
+		$rest->setAmzHeader('x-amz-acl', $acl);
+		$rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
+		if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
+			$rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
+
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return isset($rest->body->LastModified, $rest->body->ETag) ? array(
+			'time' => strtotime((string)$rest->body->LastModified),
+			'hash' => substr((string)$rest->body->ETag, 1, -1)
+		) : false;
+	}
+
+
+	/**
+	* Set up a bucket redirection
+	*
+	* @param string $bucket Bucket name
+	* @param string $location Target host name
+	* @return boolean
+	*/
+	public static function setBucketRedirect($bucket = NULL, $location = NULL)
+	{
+		$rest = new S3Request('PUT', $bucket, '', self::$endpoint);
+
+		if( empty($bucket) || empty($location) ) {
+			self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__);
+			return false;
+		}
+
+		$dom = new DOMDocument;
+		$websiteConfiguration = $dom->createElement('WebsiteConfiguration');
+		$redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo');
+		$hostName = $dom->createElement('HostName', $location);
+		$redirectAllRequestsTo->appendChild($hostName);
+		$websiteConfiguration->appendChild($redirectAllRequestsTo);
+		$dom->appendChild($websiteConfiguration);
+		$rest->setParameter('website', null);
+		$rest->data = $dom->saveXML();
+		$rest->size = strlen($rest->data);
+		$rest->setHeader('Content-Type', 'application/xml');
+		$rest = $rest->getResponse();
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Set logging for a bucket
+	*
+	* @param string $bucket Bucket name
+	* @param string $targetBucket Target bucket (where logs are stored)
+	* @param string $targetPrefix Log prefix (e,g; domain.com-)
+	* @return boolean
+	*/
+	public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null)
+	{
+		// The S3 log delivery group has to be added to the target bucket's ACP
+		if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false)
+		{
+			// Only add permissions to the target bucket when they do not exist
+			$aclWriteSet = false;
+			$aclReadSet = false;
+			foreach ($acp['acl'] as $acl)
+			if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery')
+			{
+				if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
+				elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
+			}
+			if (!$aclWriteSet) $acp['acl'][] = array(
+				'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
+			);
+			if (!$aclReadSet) $acp['acl'][] = array(
+				'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
+			);
+			if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp);
+		}
+
+		$dom = new DOMDocument;
+		$bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
+		$bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
+		if ($targetBucket !== null)
+		{
+			if ($targetPrefix == null) $targetPrefix = $bucket . '-';
+			$loggingEnabled = $dom->createElement('LoggingEnabled');
+			$loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
+			$loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
+			// TODO: Add TargetGrants?
+			$bucketLoggingStatus->appendChild($loggingEnabled);
+		}
+		$dom->appendChild($bucketLoggingStatus);
+
+		$rest = new S3Request('PUT', $bucket, '', self::$endpoint);
+		$rest->setParameter('logging', null);
+		$rest->data = $dom->saveXML();
+		$rest->size = strlen($rest->data);
+		$rest->setHeader('Content-Type', 'application/xml');
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Get logging status for a bucket
+	*
+	* This will return false if logging is not enabled.
+	* Note: To enable logging, you also need to grant write access to the log group
+	*
+	* @param string $bucket Bucket name
+	* @return array | false
+	*/
+	public static function getBucketLogging($bucket)
+	{
+		$rest = new S3Request('GET', $bucket, '', self::$endpoint);
+		$rest->setParameter('logging', null);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		if (!isset($rest->body->LoggingEnabled)) return false; // No logging
+		return array(
+			'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket,
+			'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix,
+		);
+	}
+
+
+	/**
+	* Disable bucket logging
+	*
+	* @param string $bucket Bucket name
+	* @return boolean
+	*/
+	public static function disableBucketLogging($bucket)
+	{
+		return self::setBucketLogging($bucket, null);
+	}
+
+
+	/**
+	* Get a bucket's location
+	*
+	* @param string $bucket Bucket name
+	* @return string | false
+	*/
+	public static function getBucketLocation($bucket)
+	{
+		$rest = new S3Request('GET', $bucket, '', self::$endpoint);
+		$rest->setParameter('location', null);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US';
+	}
+
+
+	/**
+	* Set object or bucket Access Control Policy
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy)
+	* @return boolean
+	*/
+	public static function setAccessControlPolicy($bucket, $uri = '', $acp = array())
+	{
+		$dom = new DOMDocument;
+		$dom->formatOutput = true;
+		$accessControlPolicy = $dom->createElement('AccessControlPolicy');
+		$accessControlList = $dom->createElement('AccessControlList');
+
+		// It seems the owner has to be passed along too
+		$owner = $dom->createElement('Owner');
+		$owner->appendChild($dom->createElement('ID', $acp['owner']['id']));
+		$owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name']));
+		$accessControlPolicy->appendChild($owner);
+
+		foreach ($acp['acl'] as $g)
+		{
+			$grant = $dom->createElement('Grant');
+			$grantee = $dom->createElement('Grantee');
+			$grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
+			if (isset($g['id']))
+			{ // CanonicalUser (DisplayName is omitted)
+				$grantee->setAttribute('xsi:type', 'CanonicalUser');
+				$grantee->appendChild($dom->createElement('ID', $g['id']));
+			}
+			elseif (isset($g['email']))
+			{ // AmazonCustomerByEmail
+				$grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail');
+				$grantee->appendChild($dom->createElement('EmailAddress', $g['email']));
+			}
+			elseif ($g['type'] == 'Group')
+			{ // Group
+				$grantee->setAttribute('xsi:type', 'Group');
+				$grantee->appendChild($dom->createElement('URI', $g['uri']));
+			}
+			$grant->appendChild($grantee);
+			$grant->appendChild($dom->createElement('Permission', $g['permission']));
+			$accessControlList->appendChild($grant);
+		}
+
+		$accessControlPolicy->appendChild($accessControlList);
+		$dom->appendChild($accessControlPolicy);
+
+		$rest = new S3Request('PUT', $bucket, $uri, self::$endpoint);
+		$rest->setParameter('acl', null);
+		$rest->data = $dom->saveXML();
+		$rest->size = strlen($rest->data);
+		$rest->setHeader('Content-Type', 'application/xml');
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Get object or bucket Access Control Policy
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @return mixed | false
+	*/
+	public static function getAccessControlPolicy($bucket, $uri = '')
+	{
+		$rest = new S3Request('GET', $bucket, $uri, self::$endpoint);
+		$rest->setParameter('acl', null);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+
+		$acp = array();
+		if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName))
+			$acp['owner'] = array(
+				'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName
+			);
+
+		if (isset($rest->body->AccessControlList))
+		{
+			$acp['acl'] = array();
+			foreach ($rest->body->AccessControlList->Grant as $grant)
+			{
+				foreach ($grant->Grantee as $grantee)
+				{
+					if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser
+						$acp['acl'][] = array(
+							'type' => 'CanonicalUser',
+							'id' => (string)$grantee->ID,
+							'name' => (string)$grantee->DisplayName,
+							'permission' => (string)$grant->Permission
+						);
+					elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail
+						$acp['acl'][] = array(
+							'type' => 'AmazonCustomerByEmail',
+							'email' => (string)$grantee->EmailAddress,
+							'permission' => (string)$grant->Permission
+						);
+					elseif (isset($grantee->URI)) // Group
+						$acp['acl'][] = array(
+							'type' => 'Group',
+							'uri' => (string)$grantee->URI,
+							'permission' => (string)$grant->Permission
+						);
+					else continue;
+				}
+			}
+		}
+		return $acp;
+	}
+
+
+	/**
+	* Delete an object
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @return boolean
+	*/
+	public static function deleteObject($bucket, $uri)
+	{
+		$rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint);
+		$rest = $rest->getResponse();
+		if ($rest->error === false && $rest->code !== 204)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::deleteObject(): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Get a query string authenticated URL
+	*
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param integer $lifetime Lifetime in seconds
+	* @param boolean $hostBucket Use the bucket name as the hostname
+	* @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification)
+	* @return string
+	*/
+	public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false)
+	{
+		$expires = self::__getTime() + $lifetime;
+		$uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri));
+		return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s',
+		// $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires,
+		$hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires,
+		urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}")));
+	}
+
+
+	/**
+	* Get a CloudFront signed policy URL
+	*
+	* @param array $policy Policy
+	* @return string
+	*/
+	public static function getSignedPolicyURL($policy)
+	{
+		$data = json_encode($policy);
+		$signature = '';
+		if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false;
+
+		$encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data));
+		$signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature));
+
+		$url = $policy['Statement'][0]['Resource'] . '?';
+		foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v)
+			$url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&';
+		return substr($url, 0, -1);
+	}
+
+
+	/**
+	* Get a CloudFront canned policy URL
+	*
+	* @param string $url URL to sign
+	* @param integer $lifetime URL lifetime
+	* @return string
+	*/
+	public static function getSignedCannedURL($url, $lifetime)
+	{
+		return self::getSignedPolicyURL(array(
+			'Statement' => array(
+				array('Resource' => $url, 'Condition' => array(
+					'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime)
+				))
+			)
+		));
+	}
+
+
+	/**
+	* Get upload POST parameters for form uploads
+	*
+	* @param string $bucket Bucket name
+	* @param string $uriPrefix Object URI prefix
+	* @param constant $acl ACL constant
+	* @param integer $lifetime Lifetime in seconds
+	* @param integer $maxFileSize Maximum filesize in bytes (default 5MB)
+	* @param string $successRedirect Redirect URL or 200 / 201 status code
+	* @param array $amzHeaders Array of x-amz-meta-* headers
+	* @param array $headers Array of request headers or content type as a string
+	* @param boolean $flashVars Includes additional "Filename" variable posted by Flash
+	* @return object
+	*/
+	public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600,
+	$maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false)
+	{
+		// Create policy object
+		$policy = new stdClass;
+		$policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime));
+		$policy->conditions = array();
+		$obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj);
+		$obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj);
+
+		$obj = new stdClass; // 200 for non-redirect uploads
+		if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
+			$obj->success_action_status = (string)$successRedirect;
+		else // URL
+			$obj->success_action_redirect = $successRedirect;
+		array_push($policy->conditions, $obj);
+
+		if ($acl !== self::ACL_PUBLIC_READ)
+			array_push($policy->conditions, array('eq', '$acl', $acl));
+
+		array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
+		if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', ''));
+		foreach (array_keys($headers) as $headerKey)
+			array_push($policy->conditions, array('starts-with', '$'.$headerKey, ''));
+		foreach ($amzHeaders as $headerKey => $headerVal)
+		{
+			$obj = new stdClass;
+			$obj->{$headerKey} = (string)$headerVal;
+			array_push($policy->conditions, $obj);
+		}
+		array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
+		$policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
+
+		// Create parameters
+		$params = new stdClass;
+		$params->AWSAccessKeyId = self::$__accessKey;
+		$params->key = $uriPrefix.'${filename}';
+		$params->acl = $acl;
+		$params->policy = $policy; unset($policy);
+		$params->signature = self::__getHash($params->policy);
+		if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201)))
+			$params->success_action_status = (string)$successRedirect;
+		else
+			$params->success_action_redirect = $successRedirect;
+		foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
+		foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal;
+		return $params;
+	}
+
+
+	/**
+	* Create a CloudFront distribution
+	*
+	* @param string $bucket Bucket name
+	* @param boolean $enabled Enabled (true/false)
+	* @param array $cnames Array containing CNAME aliases
+	* @param string $comment Use the bucket name as the hostname
+	* @param string $defaultRootObject Default root object
+	* @param string $originAccessIdentity Origin access identity
+	* @param array $trustedSigners Array of trusted signers
+	* @return array | false
+	*/
+	public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+		$useSSL = self::$useSSL;
+
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
+		$rest->data = self::__getCloudFrontDistributionConfigXML(
+			$bucket.'.s3.amazonaws.com',
+			$enabled,
+			(string)$comment,
+			(string)microtime(true),
+			$cnames,
+			$defaultRootObject,
+			$originAccessIdentity,
+			$trustedSigners
+		);
+
+		$rest->size = strlen($rest->data);
+		$rest->setHeader('Content-Type', 'application/xml');
+		$rest = self::__getCloudFrontResponse($rest);
+
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 201)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		} elseif ($rest->body instanceof SimpleXMLElement)
+			return self::__parseCloudFrontDistributionConfig($rest->body);
+		return false;
+	}
+
+
+	/**
+	* Get CloudFront distribution info
+	*
+	* @param string $distributionId Distribution ID from listDistributions()
+	* @return array | false
+	*/
+	public static function getDistribution($distributionId)
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::getDistribution($distributionId): %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+		$useSSL = self::$useSSL;
+
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com');
+		$rest = self::__getCloudFrontResponse($rest);
+
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		elseif ($rest->body instanceof SimpleXMLElement)
+		{
+			$dist = self::__parseCloudFrontDistributionConfig($rest->body);
+			$dist['hash'] = $rest->headers['hash'];
+			$dist['id'] = $distributionId;
+			return $dist;
+		}
+		return false;
+	}
+
+
+	/**
+	* Update a CloudFront distribution
+	*
+	* @param array $dist Distribution array info identical to output of getDistribution()
+	* @return array | false
+	*/
+	public static function updateDistribution($dist)
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		$useSSL = self::$useSSL;
+
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com');
+		$rest->data = self::__getCloudFrontDistributionConfigXML(
+			$dist['origin'],
+			$dist['enabled'],
+			$dist['comment'],
+			$dist['callerReference'],
+			$dist['cnames'],
+			$dist['defaultRootObject'],
+			$dist['originAccessIdentity'],
+			$dist['trustedSigners']
+		);
+
+		$rest->size = strlen($rest->data);
+		$rest->setHeader('If-Match', $dist['hash']);
+		$rest = self::__getCloudFrontResponse($rest);
+
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		} else {
+			$dist = self::__parseCloudFrontDistributionConfig($rest->body);
+			$dist['hash'] = $rest->headers['hash'];
+			return $dist;
+		}
+		return false;
+	}
+
+
+	/**
+	* Delete a CloudFront distribution
+	*
+	* @param array $dist Distribution array info identical to output of getDistribution()
+	* @return boolean
+	*/
+	public static function deleteDistribution($dist)
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		$useSSL = self::$useSSL;
+
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com');
+		$rest->setHeader('If-Match', $dist['hash']);
+		$rest = self::__getCloudFrontResponse($rest);
+
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 204)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Get a list of CloudFront distributions
+	*
+	* @return array
+	*/
+	public static function listDistributions()
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		$useSSL = self::$useSSL;
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com');
+		$rest = self::__getCloudFrontResponse($rest);
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			self::__triggerError(sprintf("S3::listDistributions(): [%s] %s",
+			$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
+			return false;
+		}
+		elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary))
+		{
+			$list = array();
+			if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated))
+			{
+				//$info['marker'] = (string)$rest->body->Marker;
+				//$info['maxItems'] = (int)$rest->body->MaxItems;
+				//$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false;
+			}
+			foreach ($rest->body->DistributionSummary as $summary)
+				$list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary);
+
+			return $list;
+		}
+		return array();
+	}
+
+	/**
+	* List CloudFront Origin Access Identities
+	*
+	* @return array
+	*/
+	public static function listOriginAccessIdentities()
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com');
+		$rest = self::__getCloudFrontResponse($rest);
+		$useSSL = self::$useSSL;
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s",
+			$rest->error['code'], $rest->error['message']), E_USER_WARNING);
+			return false;
+		}
+
+		if (isset($rest->body->CloudFrontOriginAccessIdentitySummary))
+		{
+			$identities = array();
+			foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity)
+				if (isset($identity->S3CanonicalUserId))
+					$identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId);
+			return $identities;
+		}
+		return false;
+	}
+
+
+	/**
+	* Invalidate objects in a CloudFront distribution
+	*
+	* Thanks to Martin Lindkvist for S3::invalidateDistribution()
+	*
+	* @param string $distributionId Distribution ID from listDistributions()
+	* @param array $paths Array of object paths to invalidate
+	* @return boolean
+	*/
+	public static function invalidateDistribution($distributionId, $paths)
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		$useSSL = self::$useSSL;
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
+		$rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true));
+		$rest->size = strlen($rest->data);
+		$rest = self::__getCloudFrontResponse($rest);
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 201)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s",
+			$rest->error['code'], $rest->error['message']), E_USER_WARNING);
+			return false;
+		}
+		return true;
+	}
+
+
+	/**
+	* Get a InvalidationBatch DOMDocument
+	*
+	* @internal Used to create XML in invalidateDistribution()
+	* @param array $paths Paths to objects to invalidateDistribution
+	* @param int $callerReference
+	* @return string
+	*/
+	private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0')
+	{
+		$dom = new DOMDocument('1.0', 'UTF-8');
+		$dom->formatOutput = true;
+		$invalidationBatch = $dom->createElement('InvalidationBatch');
+		foreach ($paths as $path)
+			$invalidationBatch->appendChild($dom->createElement('Path', $path));
+
+		$invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference));
+		$dom->appendChild($invalidationBatch);
+		return $dom->saveXML();
+	}
+
+
+	/**
+	* List your invalidation batches for invalidateDistribution() in a CloudFront distribution
+	*
+	* http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html
+	* returned array looks like this:
+	*	Array
+	*	(
+	*		[I31TWB0CN9V6XD] => InProgress
+	*		[IT3TFE31M0IHZ] => Completed
+	*		[I12HK7MPO1UQDA] => Completed
+	*		[I1IA7R6JKTC3L2] => Completed
+	*	)
+	*
+	* @param string $distributionId Distribution ID from listDistributions()
+	* @return array
+	*/
+	public static function getDistributionInvalidationList($distributionId)
+	{
+		if (!extension_loaded('openssl'))
+		{
+			self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s",
+			"CloudFront functionality requires SSL"), __FILE__, __LINE__);
+			return false;
+		}
+
+		$useSSL = self::$useSSL;
+		self::$useSSL = true; // CloudFront requires SSL
+		$rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com');
+		$rest = self::__getCloudFrontResponse($rest);
+		self::$useSSL = $useSSL;
+
+		if ($rest->error === false && $rest->code !== 200)
+			$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
+		if ($rest->error !== false)
+		{
+			trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]",
+			$rest->error['code'], $rest->error['message']), E_USER_WARNING);
+			return false;
+		}
+		elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary))
+		{
+			$list = array();
+			foreach ($rest->body->InvalidationSummary as $summary)
+				$list[(string)$summary->Id] = (string)$summary->Status;
+
+			return $list;
+		}
+		return array();
+	}
+
+
+	/**
+	* Get a DistributionConfig DOMDocument
+	*
+	* http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html
+	*
+	* @internal Used to create XML in createDistribution() and updateDistribution()
+	* @param string $bucket S3 Origin bucket
+	* @param boolean $enabled Enabled (true/false)
+	* @param string $comment Comment to append
+	* @param string $callerReference Caller reference
+	* @param array $cnames Array of CNAME aliases
+	* @param string $defaultRootObject Default root object
+	* @param string $originAccessIdentity Origin access identity
+	* @param array $trustedSigners Array of trusted signers
+	* @return string
+	*/
+	private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array())
+	{
+		$dom = new DOMDocument('1.0', 'UTF-8');
+		$dom->formatOutput = true;
+		$distributionConfig = $dom->createElement('DistributionConfig');
+		$distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/');
+
+		$origin = $dom->createElement('S3Origin');
+		$origin->appendChild($dom->createElement('DNSName', $bucket));
+		if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity));
+		$distributionConfig->appendChild($origin);
+
+		if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject));
+
+		$distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference));
+		foreach ($cnames as $cname)
+			$distributionConfig->appendChild($dom->createElement('CNAME', $cname));
+		if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment));
+		$distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false'));
+
+		$trusted = $dom->createElement('TrustedSigners');
+		foreach ($trustedSigners as $id => $type)
+			$trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type));
+		$distributionConfig->appendChild($trusted);
+
+		$dom->appendChild($distributionConfig);
+		//var_dump($dom->saveXML());
+		return $dom->saveXML();
+	}
+
+
+	/**
+	* Parse a CloudFront distribution config
+	*
+	* See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html
+	*
+	* @internal Used to parse the CloudFront DistributionConfig node to an array
+	* @param object &$node DOMNode
+	* @return array
+	*/
+	private static function __parseCloudFrontDistributionConfig(&$node)
+	{
+		if (isset($node->DistributionConfig))
+			return self::__parseCloudFrontDistributionConfig($node->DistributionConfig);
+
+		$dist = array();
+		if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName))
+		{
+			$dist['id'] = (string)$node->Id;
+			$dist['status'] = (string)$node->Status;
+			$dist['time'] = strtotime((string)$node->LastModifiedTime);
+			$dist['domain'] = (string)$node->DomainName;
+		}
+
+		if (isset($node->CallerReference))
+			$dist['callerReference'] = (string)$node->CallerReference;
+
+		if (isset($node->Enabled))
+			$dist['enabled'] = (string)$node->Enabled == 'true' ? true : false;
+
+		if (isset($node->S3Origin))
+		{
+			if (isset($node->S3Origin->DNSName))
+				$dist['origin'] = (string)$node->S3Origin->DNSName;
+
+			$dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ?
+			(string)$node->S3Origin->OriginAccessIdentity : null;
+		}
+
+		$dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null;
+
+		$dist['cnames'] = array();
+		if (isset($node->CNAME))
+			foreach ($node->CNAME as $cname)
+				$dist['cnames'][(string)$cname] = (string)$cname;
+
+		$dist['trustedSigners'] = array();
+		if (isset($node->TrustedSigners))
+			foreach ($node->TrustedSigners as $signer)
+			{
+				if (isset($signer->Self))
+					$dist['trustedSigners'][''] = 'Self';
+				elseif (isset($signer->KeyPairId))
+					$dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId';
+				elseif (isset($signer->AwsAccountNumber))
+					$dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber';
+			}
+
+		$dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null;
+		return $dist;
+	}
+
+
+	/**
+	* Grab CloudFront response
+	*
+	* @internal Used to parse the CloudFront S3Request::getResponse() output
+	* @param object &$rest S3Request instance
+	* @return object
+	*/
+	private static function __getCloudFrontResponse(&$rest)
+	{
+		$rest->getResponse();
+		if ($rest->response->error === false && isset($rest->response->body) &&
+		is_string($rest->response->body) && substr($rest->response->body, 0, 5) == '<?xml')
+		{
+			$rest->response->body = simplexml_load_string($rest->response->body);
+			// Grab CloudFront errors
+			if (isset($rest->response->body->Error, $rest->response->body->Error->Code,
+			$rest->response->body->Error->Message))
+			{
+				$rest->response->error = array(
+					'code' => (string)$rest->response->body->Error->Code,
+					'message' => (string)$rest->response->body->Error->Message
+				);
+				unset($rest->response->body);
+			}
+		}
+		return $rest->response;
+	}
+
+
+	/**
+	* Get MIME type for file
+	*
+	* To override the putObject() Content-Type, add it to $requestHeaders
+	*
+	* To use fileinfo, ensure the MAGIC environment variable is set
+	*
+	* @internal Used to get mime types
+	* @param string &$file File path
+	* @return string
+	*/
+	private static function __getMIMEType(&$file)
+	{
+		static $exts = array(
+			'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif',
+			'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf',
+			'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml',
+			'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', 
+			'zip' => 'application/zip', 'gz' => 'application/x-gzip',
+			'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
+			'bz2' => 'application/x-bzip2',  'rar' => 'application/x-rar-compressed',
+			'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload',
+			'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain',
+			'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
+			'css' => 'text/css', 'js' => 'text/javascript',
+			'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
+			'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
+			'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
+			'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
+		);
+
+		$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
+		if (isset($exts[$ext])) return $exts[$ext];
+
+		// Use fileinfo if available
+		if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
+		($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false)
+		{
+			if (($type = finfo_file($finfo, $file)) !== false)
+			{
+				// Remove the charset and grab the last content-type
+				$type = explode(' ', str_replace('; charset=', ';charset=', $type));
+				$type = array_pop($type);
+				$type = explode(';', $type);
+				$type = trim(array_shift($type));
+			}
+			finfo_close($finfo);
+			if ($type !== false && strlen($type) > 0) return $type;
+		}
+
+		return 'application/octet-stream';
+	}
+
+
+	/**
+	* Get the current time
+	*
+	* @internal Used to apply offsets to sytem time
+	* @return integer
+	*/
+	public static function __getTime()
+	{
+		return time() + self::$__timeOffset;
+	}
+
+
+	/**
+	* Generate the auth string: "AWS AccessKey:Signature"
+	*
+	* @internal Used by S3Request::getResponse()
+	* @param string $string String to sign
+	* @return string
+	*/
+	public static function __getSignature($string)
+	{
+		return 'AWS '.self::$__accessKey.':'.self::__getHash($string);
+	}
+
+
+	/**
+	* Creates a HMAC-SHA1 hash
+	*
+	* This uses the hash extension if loaded
+	*
+	* @internal Used by __getSignature()
+	* @param string $string String to sign
+	* @return string
+	*/
+	private static function __getHash($string)
+	{
+		return base64_encode(extension_loaded('hash') ?
+		hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1(
+		(str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
+		pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^
+		(str_repeat(chr(0x36), 64))) . $string)))));
+	}
+
+}
+
+/**
+ * S3 Request class 
+ *
+ * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
+ * @version 0.5.0-dev
+ */
+final class S3Request
+{
+	/**
+	 * AWS URI
+	 *
+	 * @var string
+	 * @access private
+	 */
+	private $endpoint;
+	
+	/**
+	 * Verb
+	 *
+	 * @var string
+	 * @access private
+	 */
+	private $verb;
+	
+	/**
+	 * S3 bucket name
+	 *
+	 * @var string
+	 * @access private
+	 */
+	private $bucket;
+	
+	/**
+	 * Object URI
+	 *
+	 * @var string
+	 * @access private
+	 */
+	private $uri;
+	
+	/**
+	 * Final object URI
+	 *
+	 * @var string
+	 * @access private
+	 */
+	private $resource = '';
+	
+	/**
+	 * Additional request parameters
+	 *
+	 * @var array
+	 * @access private
+	 */
+	private $parameters = array();
+	
+	/**
+	 * Amazon specific request headers
+	 *
+	 * @var array
+	 * @access private
+	 */
+	private $amzHeaders = array();
+
+	/**
+	 * HTTP request headers
+	 *
+	 * @var array
+	 * @access private
+	 */
+	private $headers = array(
+		'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => ''
+	);
+
+	/**
+	 * Use HTTP PUT?
+	 *
+	 * @var bool
+	 * @access public
+	 */
+	public $fp = false;
+
+	/**
+	 * PUT file size
+	 *
+	 * @var int
+	 * @access public
+	 */
+	public $size = 0;
+
+	/**
+	 * PUT post fields
+	 *
+	 * @var array
+	 * @access public
+	 */
+	public $data = false;
+
+	/**
+	 * S3 request respone
+	 *
+	 * @var object
+	 * @access public
+	 */
+	public $response;
+
+
+	/**
+	* Constructor
+	*
+	* @param string $verb Verb
+	* @param string $bucket Bucket name
+	* @param string $uri Object URI
+	* @param string $endpoint AWS endpoint URI
+	* @return mixed
+	*/
+	function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com')
+	{
+		
+		$this->endpoint = $endpoint;
+		$this->verb = $verb;
+		$this->bucket = $bucket;
+		$this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/';
+
+		//if ($this->bucket !== '')
+		//	$this->resource = '/'.$this->bucket.$this->uri;
+		//else
+		//	$this->resource = $this->uri;
+
+		if ($this->bucket !== '')
+		{
+			if ($this->__dnsBucketName($this->bucket))
+			{
+				$this->headers['Host'] = $this->bucket.'.'.$this->endpoint;
+				$this->resource = '/'.$this->bucket.$this->uri;
+			}
+			else
+			{
+				$this->headers['Host'] = $this->endpoint;
+				$this->uri = $this->uri;
+				if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri;
+				$this->bucket = '';
+				$this->resource = $this->uri;
+			}
+		}
+		else
+		{
+			$this->headers['Host'] = $this->endpoint;
+			$this->resource = $this->uri;
+		}
+
+
+		$this->headers['Date'] = gmdate('D, d M Y H:i:s T');
+		$this->response = new STDClass;
+		$this->response->error = false;
+		$this->response->body = null;
+		$this->response->headers = array();
+	}
+
+
+	/**
+	* Set request parameter
+	*
+	* @param string $key Key
+	* @param string $value Value
+	* @return void
+	*/
+	public function setParameter($key, $value)
+	{
+		$this->parameters[$key] = $value;
+	}
+
+
+	/**
+	* Set request header
+	*
+	* @param string $key Key
+	* @param string $value Value
+	* @return void
+	*/
+	public function setHeader($key, $value)
+	{
+		$this->headers[$key] = $value;
+	}
+
+
+	/**
+	* Set x-amz-meta-* header
+	*
+	* @param string $key Key
+	* @param string $value Value
+	* @return void
+	*/
+	public function setAmzHeader($key, $value)
+	{
+		$this->amzHeaders[$key] = $value;
+	}
+
+
+	/**
+	* Get the S3 response
+	*
+	* @return object | false
+	*/
+	public function getResponse()
+	{
+		$query = '';
+		if (sizeof($this->parameters) > 0)
+		{
+			$query = substr($this->uri, -1) !== '?' ? '?' : '&';
+			foreach ($this->parameters as $var => $value)
+				if ($value == null || $value == '') $query .= $var.'&';
+				else $query .= $var.'='.rawurlencode($value).'&';
+			$query = substr($query, 0, -1);
+			$this->uri .= $query;
+
+			if (array_key_exists('acl', $this->parameters) ||
+			array_key_exists('location', $this->parameters) ||
+			array_key_exists('torrent', $this->parameters) ||
+			array_key_exists('website', $this->parameters) ||
+			array_key_exists('logging', $this->parameters))
+				$this->resource .= $query;
+		}
+		$url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri;
+
+		//var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url);
+
+		// Basic setup
+		$curl = curl_init();
+		curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
+
+		if (S3::$useSSL)
+		{
+			// Set protocol version
+			curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion);
+
+			// SSL Validation can now be optional for those with broken OpenSSL installations
+			curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0);
+			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0);
+
+			if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey);
+			if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert);
+			if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert);
+		}
+
+		curl_setopt($curl, CURLOPT_URL, $url);
+
+		if (S3::$proxy != null && isset(S3::$proxy['host']))
+		{
+			curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']);
+			curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']);
+			if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null)
+				curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass']));
+		}
+
+		// Headers
+		$headers = array(); $amz = array();
+		foreach ($this->amzHeaders as $header => $value)
+			if (strlen($value) > 0) $headers[] = $header.': '.$value;
+		foreach ($this->headers as $header => $value)
+			if (strlen($value) > 0) $headers[] = $header.': '.$value;
+
+		// Collect AMZ headers for signature
+		foreach ($this->amzHeaders as $header => $value)
+			if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value;
+
+		// AMZ headers must be sorted
+		if (sizeof($amz) > 0)
+		{
+			//sort($amz);
+			usort($amz, array(&$this, '__sortMetaHeadersCmp'));
+			$amz = "\n".implode("\n", $amz);
+		} else $amz = '';
+
+		if (S3::hasAuth())
+		{
+			// Authorization string (CloudFront stringToSign should only contain a date)
+			if ($this->headers['Host'] == 'cloudfront.amazonaws.com')
+				$headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']);
+			else
+			{
+				$headers[] = 'Authorization: ' . S3::__getSignature(
+					$this->verb."\n".
+					$this->headers['Content-MD5']."\n".
+					$this->headers['Content-Type']."\n".
+					$this->headers['Date'].$amz."\n".
+					$this->resource
+				);
+			}
+		}
+
+		curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+		curl_setopt($curl, CURLOPT_HEADER, false);
+		curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
+		curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
+		curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
+		curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
+
+		// Request types
+		switch ($this->verb)
+		{
+			case 'GET': break;
+			case 'PUT': case 'POST': // POST only used for CloudFront
+				if ($this->fp !== false)
+				{
+					curl_setopt($curl, CURLOPT_PUT, true);
+					curl_setopt($curl, CURLOPT_INFILE, $this->fp);
+					if ($this->size >= 0)
+						curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
+				}
+				elseif ($this->data !== false)
+				{
+					curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
+					curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
+				}
+				else
+					curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
+			break;
+			case 'HEAD':
+				curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
+				curl_setopt($curl, CURLOPT_NOBODY, true);
+			break;
+			case 'DELETE':
+				curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
+			break;
+			default: break;
+		}
+
+		// Execute, grab errors
+		if (curl_exec($curl))
+			$this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+		else
+			$this->response->error = array(
+				'code' => curl_errno($curl),
+				'message' => curl_error($curl),
+				'resource' => $this->resource
+			);
+
+		@curl_close($curl);
+
+		// Parse body into XML
+		if ($this->response->error === false && isset($this->response->headers['type']) &&
+		$this->response->headers['type'] == 'application/xml' && isset($this->response->body))
+		{
+			$this->response->body = simplexml_load_string($this->response->body);
+
+			// Grab S3 errors
+			if (!in_array($this->response->code, array(200, 204, 206)) &&
+			isset($this->response->body->Code, $this->response->body->Message))
+			{
+				$this->response->error = array(
+					'code' => (string)$this->response->body->Code,
+					'message' => (string)$this->response->body->Message
+				);
+				if (isset($this->response->body->Resource))
+					$this->response->error['resource'] = (string)$this->response->body->Resource;
+				unset($this->response->body);
+			}
+		}
+
+		// Clean up file resources
+		if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp);
+
+		return $this->response;
+	}
+
+	/**
+	* Sort compare for meta headers
+	*
+	* @internal Used to sort x-amz meta headers
+	* @param string $a String A
+	* @param string $b String B
+	* @return integer
+	*/
+	private function __sortMetaHeadersCmp($a, $b)
+	{
+		$lenA = strpos($a, ':');
+		$lenB = strpos($b, ':');
+		$minLen = min($lenA, $lenB);
+		$ncmp = strncmp($a, $b, $minLen);
+		if ($lenA == $lenB) return $ncmp;
+		if (0 == $ncmp) return $lenA < $lenB ? -1 : 1;
+		return $ncmp;
+	}
+
+	/**
+	* CURL write callback
+	*
+	* @param resource &$curl CURL resource
+	* @param string &$data Data
+	* @return integer
+	*/
+	private function __responseWriteCallback(&$curl, &$data)
+	{
+		if (in_array($this->response->code, array(200, 206)) && $this->fp !== false)
+			return fwrite($this->fp, $data);
+		else
+			$this->response->body .= $data;
+		return strlen($data);
+	}
+
+
+	/**
+	* Check DNS conformity
+	*
+	* @param string $bucket Bucket name
+	* @return boolean
+	*/
+	private function __dnsBucketName($bucket)
+	{
+		if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false;
+		if (S3::$useSSL && strstr($bucket, '.') !== false) return false;
+		if (strstr($bucket, '-.') !== false) return false;
+		if (strstr($bucket, '..') !== false) return false;
+		if (!preg_match("/^[0-9a-z]/", $bucket)) return false;
+		if (!preg_match("/[0-9a-z]$/", $bucket)) return false;
+		return true;
+	}
+
+
+	/**
+	* CURL header callback
+	*
+	* @param resource $curl CURL resource
+	* @param string $data Data
+	* @return integer
+	*/
+	private function __responseHeaderCallback($curl, $data)
+	{
+		if (($strlen = strlen($data)) <= 2) return $strlen;
+		if (substr($data, 0, 4) == 'HTTP')
+			$this->response->code = (int)substr($data, 9, 3);
+		else
+		{
+			$data = trim($data);
+			if (strpos($data, ': ') === false) return $strlen;
+			list($header, $value) = explode(': ', $data, 2);
+			if ($header == 'Last-Modified')
+				$this->response->headers['time'] = strtotime($value);
+			elseif ($header == 'Date')
+				$this->response->headers['date'] = strtotime($value);
+			elseif ($header == 'Content-Length')
+				$this->response->headers['size'] = (int)$value;
+			elseif ($header == 'Content-Type')
+				$this->response->headers['type'] = $value;
+			elseif ($header == 'ETag')
+				$this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value;
+			elseif (preg_match('/^x-amz-meta-.*$/', $header))
+				$this->response->headers[$header] = $value;
+		}
+		return $strlen;
+	}
+
+}
+
+/**
+ * S3 exception class
+ *
+ * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
+ * @version 0.5.0-dev
+ */
+
+class S3Exception extends Exception {
+	/**
+	 * Class constructor
+	 *
+	 * @param string $message Exception message
+	 * @param string $file File in which exception was created
+	 * @param string $line Line number on which exception was created
+	 * @param int $code Exception code
+	 */
+	function __construct($message, $file, $line, $code = 0)
+	{
+		parent::__construct($message, $code);
+		$this->file = $file;
+		$this->line = $line;
+	}
+}
+

+ 113 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee.php

@@ -0,0 +1,113 @@
+<?php
+
+function checkExtentions() {
+    $extensions = array('curl', 'json');
+    foreach ($extensions AS $e) {
+        if (!extension_loaded($e)) {
+            throw new Exception('ChargeBee requires the ' . $e . ' extension.');
+        }
+    }
+}
+
+checkExtentions();
+
+abstract class ChargeBee
+{
+
+  public static $verifyCaCerts = true;
+
+  public static function getVerifyCaCerts() {
+    return self::$verifyCaCerts;
+  }
+
+  public static function setVerifyCaCerts($verify) {
+    self::$verifyCaCerts = $verify;
+  }
+
+	public static function getCaCertPath() {
+		return dirname(__FILE__) . "/ssl/ca-certs.crt";
+	}
+
+}
+
+require(dirname(__FILE__) . '/ChargeBee/Version.php');
+require(dirname(__FILE__) . '/ChargeBee/Environment.php');
+require(dirname(__FILE__) . '/ChargeBee/Util.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/IOException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/APIError.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/PaymentException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/OperationFailedException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/InvalidRequestException.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Request.php');
+require(dirname(__FILE__) . '/ChargeBee/Curl.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Result.php');
+require(dirname(__FILE__) . '/ChargeBee/ListResult.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Model.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Models/Subscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionCoupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Customer.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerContact.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerPaymentMethod.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Card.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ThirdPartyPaymentMethod.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Address.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Invoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedRefund.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Order.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLinkedOrder.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Estimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Transaction.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedInvoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Content.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Event.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/EventWebhook.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/HostedPage.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Plan.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Addon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Coupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponCode.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Comment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PortalSession.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PortalSessionLinkedCustomer.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Download.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteAllocation.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLinkedRefund.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceAdjustmentCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceAppliedCredit.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceCreatedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceIssuedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLinkedPayment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEstimateShippingAddress.php');
+

+ 173 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Model.php

@@ -0,0 +1,173 @@
+<?php
+
+class ChargeBee_Model
+{
+
+	protected $_values;
+	
+	protected $_subTypes;
+	protected $_dependantTypes;
+
+	protected $_data = array();
+
+	protected $allowed = array();
+
+	function __construct($values, $subTypes=array(), $dependantTypes=array())
+	{
+		$this->_values = $values;
+		$this->_subTypes = $subTypes;
+		$this->_dependantTypes=$dependantTypes;
+		$this->_load();
+	}
+	
+	public function param($k)
+	{
+	  if(array_key_exists($k, $this->_values))
+	  {
+	    return $this->_values[$k];
+	  }
+	  else
+	  {
+	    throw new Exception("Unknown param $k in " . get_class($this));
+	  }
+	}
+
+	public function getValues()
+  	{
+    	return $this->_values;
+  	}
+
+  	public function toJson() { 
+  		return json_encode($this->_values); 
+  	}
+	
+	public function __set($k, $v)
+	{
+		$this->_data[$k] = $v;
+	}
+	
+	public function __isset($k)
+	{
+		return isset($this->_data[$k]);
+	}
+	
+	public function __unset($k)
+	{
+		unset($this->data[$k]);
+	}
+	
+	public function __get($k)
+	{
+		if (isset($this->_data[$k])) 
+		{
+			return $this->_data[$k];
+		} 
+		else if(in_array($k, $this->allowed))
+		{
+			return null;
+		}
+        else if(substr($k, 0, 2) == "cf") //All the custom fields start with prefix cf. 
+        { 
+               return null;
+        } 
+		else
+		{
+			throw new Exception("Unknown property $k in " . get_class($this));
+		}
+	}
+	
+	private function __getDependant($k)
+	{
+		if (isset($this->_dependantTypes[$k])) 
+		{
+			return $this->_dependantTypes[$k];
+		} 
+		else if(in_array($k, $this->allowed))
+		{
+			return null;
+		}
+		else
+		{
+			throw new Exception("Unknown property $k in " . get_class($this));
+		}
+	}
+	
+  private function isHash($array)
+  {
+    if (!is_array($array))
+      return false;
+    foreach (array_keys($array) as $k) {
+      if (is_numeric($k))
+        return false;
+    }
+    return true;
+  }
+
+	private function _load()
+	{
+		foreach($this->_values as $k => $v) 
+		{
+			$setVal = null;
+			if($this->isHash($v) && array_key_exists($k, $this->_dependantTypes))
+			{
+				continue;
+			}
+			if($this->isHash($v) && array_key_exists($k, $this->_subTypes))
+			{
+				$setVal = new $this->_subTypes[$k]($v);
+			}
+			else if(is_array($v) && array_key_exists($k, $this->_subTypes))
+			{
+					$setVal = array();
+					foreach($v as $stV)
+					{
+						array_push($setVal, new $this->_subTypes[$k]($stV));
+					}
+			}
+			if(is_null($setVal))
+			{
+				$setVal = $v;
+			}
+			$this->_data[ChargeBee_Util::toCamelCaseFromUnderscore($k)] = $setVal;
+		}
+	}
+	
+	public function _initDependant($obj, $type, $subTypes = array())
+	{
+		if(!array_key_exists($type, $obj))
+		{
+	    	return $this;
+		}
+		$class=$this->__getDependant($type);
+		if($this->isHash($obj) && !is_null($class))
+		{
+			$dependantObj = new $class($obj[$type], $subTypes);
+			$this->_data[ChargeBee_Util::toCamelCaseFromUnderscore($type)] = $dependantObj;
+		}
+		return $this;
+	}
+	
+	public function _initDependantList($obj, $type, $subTypes = array())
+	{
+		if(!array_key_exists($type, $obj))
+		{
+	    	return $this;
+		}
+		if(!is_array($obj[$type])){
+			return $this;
+		}
+		$class=$this->__getDependant($type);
+		if(!is_null($class)){
+			$setVal = array();
+			foreach($obj[$type] as $dt)
+			{
+				array_push($setVal, new $class($dt, $subTypes));
+			}
+			$this->_data[ChargeBee_Util::toCamelCaseFromUnderscore($type)] = $setVal;
+		}
+		return $this;
+	}	
+			
+}
+
+?>

+ 51 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Addon.php

@@ -0,0 +1,51 @@
+<?php
+
+class ChargeBee_Addon extends ChargeBee_Model
+{
+
+  protected $allowed = array('id', 'name', 'invoiceName', 'description', 'type', 'chargeType', 'price', 'currencyCode',
+'period', 'periodUnit', 'unit', 'status', 'archivedAt', 'enabledInPortal', 'taxCode', 'sku','accountingCode', 'accountingCategory1', 'accountingCategory2', 'resourceVersion', 'updatedAt','invoiceNotes', 'taxable', 'taxProfileId', 'metaData');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addons"), $params, $env, $headers);
+  }
+
+  public static function update($id, $params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addons",$id), $params, $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::sendListRequest(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("addons"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("addons",$id), array(), $env, $headers);
+  }
+
+  public static function delete($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addons",$id,"delete"), array(), $env, $headers);
+  }
+
+  public static function copy($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addons","copy"), $params, $env, $headers);
+  }
+
+  public static function unarchive($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addons",$id,"unarchive"), array(), $env, $headers);
+  }
+
+ }
+
+?>

+ 26 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Address.php

@@ -0,0 +1,26 @@
+<?php
+
+class ChargeBee_Address extends ChargeBee_Model
+{
+
+  protected $allowed = array('label', 'firstName', 'lastName', 'email', 'company', 'phone', 'addr', 'extendedAddr',
+'extendedAddr2', 'city', 'stateCode', 'state', 'country', 'zip', 'validationStatus', 'subscriptionId');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function retrieve($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("addresses"), $params, $env, $headers);
+  }
+
+  public static function update($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("addresses"), $params, $env, $headers);
+  }
+
+ }
+
+?>

+ 41 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Card.php

@@ -0,0 +1,41 @@
+<?php
+
+class ChargeBee_Card extends ChargeBee_Model
+{
+
+  protected $allowed = array('customerId', 'status', 'gateway', 'gatewayAccountId', 'firstName', 'lastName',
+'iin', 'last4', 'cardType', 'fundingType', 'expiryMonth', 'expiryYear', 'billingAddr1', 'billingAddr2','billingCity', 'billingStateCode', 'billingState', 'billingCountry', 'billingZip', 'ipAddress','maskedNumber');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("cards",$id), array(), $env, $headers);
+  }
+
+  public static function updateCardForCustomer($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"credit_card"), $params, $env, $headers);
+  }
+
+  public static function switchGatewayForCustomer($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"switch_gateway"), $params, $env, $headers);
+  }
+
+  public static function copyCardForCustomer($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"copy_card"), $params, $env, $headers);
+  }
+
+  public static function deleteCardForCustomer($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"delete_card"), array(), $env, $headers);
+  }
+
+ }
+
+?>

+ 46 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Coupon.php

@@ -0,0 +1,46 @@
+<?php
+
+class ChargeBee_Coupon extends ChargeBee_Model
+{
+
+  protected $allowed = array('id', 'name', 'invoiceName', 'discountType', 'discountPercentage', 'discountAmount',
+'discountQuantity', 'currencyCode', 'durationType', 'durationMonth', 'validTill', 'maxRedemptions','status', 'applyDiscountOn', 'applyOn', 'planConstraint', 'addonConstraint', 'createdAt', 'archivedAt','resourceVersion', 'updatedAt', 'planIds', 'addonIds', 'redemptions', 'invoiceNotes', 'metaData');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupons"), $params, $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::sendListRequest(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("coupons"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("coupons",$id), array(), $env, $headers);
+  }
+
+  public static function delete($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupons",$id,"delete"), array(), $env, $headers);
+  }
+
+  public static function copy($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupons","copy"), $params, $env, $headers);
+  }
+
+  public static function unarchive($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupons",$id,"unarchive"), array(), $env, $headers);
+  }
+
+ }
+
+?>

+ 36 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CouponCode.php

@@ -0,0 +1,36 @@
+<?php
+
+class ChargeBee_CouponCode extends ChargeBee_Model
+{
+
+  protected $allowed = array('code', 'status', 'couponId', 'couponSetName'
+);
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupon_codes"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("coupon_codes",$id), array(), $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::sendListRequest(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("coupon_codes"), $params, $env, $headers);
+  }
+
+  public static function archive($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("coupon_codes",$id,"archive"), array(), $env, $headers);
+  }
+
+ }
+
+?>

+ 41 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNote.php

@@ -0,0 +1,41 @@
+<?php
+
+class ChargeBee_CreditNote extends ChargeBee_Model
+{
+
+  protected $allowed = array('id', 'customerId', 'subscriptionId', 'referenceInvoiceId', 'type', 'reasonCode',
+'status', 'vatNumber', 'date', 'priceType', 'currencyCode', 'total', 'amountAllocated', 'amountRefunded','amountAvailable', 'refundedAt', 'voidedAt', 'resourceVersion', 'updatedAt', 'subTotal', 'lineItems','discounts', 'taxes', 'lineItemTaxes', 'linkedRefunds', 'allocations', 'deleted');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("credit_notes"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("credit_notes",$id), array(), $env, $headers);
+  }
+
+  public static function pdf($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("credit_notes",$id,"pdf"), array(), $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::sendListRequest(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("credit_notes"), $params, $env, $headers);
+  }
+
+  public static function creditNotesForCustomer($id, $params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("customers",$id,"credit_notes"), $params, $env, $headers);
+  }
+
+ }
+
+?>

+ 16 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteEstimate.php

@@ -0,0 +1,16 @@
+<?php
+
+class ChargeBee_CreditNoteEstimate extends ChargeBee_Model
+{
+
+  protected $allowed = array('referenceInvoiceId', 'type', 'priceType', 'currencyCode', 'subTotal', 'total',
+'amountAllocated', 'amountAvailable', 'lineItems', 'discounts', 'taxes', 'lineItemTaxes');
+
+
+
+  # OPERATIONS
+  #-----------
+
+ }
+
+?>

+ 9 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteEstimateLineItem.php

@@ -0,0 +1,9 @@
+<?php
+
+class ChargeBee_CreditNoteEstimateLineItem extends ChargeBee_Model
+{
+  protected $allowed = array('id', 'date_from', 'date_to', 'unit_amount', 'quantity', 'is_taxed', 'tax_amount', 'tax_rate', 'amount', 'discount_amount', 'item_level_discount_amount', 'description', 'entity_type', 'tax_exempt_reason', 'entity_id');
+
+}
+
+?>

+ 9 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/CreditNoteLineItem.php

@@ -0,0 +1,9 @@
+<?php
+
+class ChargeBee_CreditNoteLineItem extends ChargeBee_Model
+{
+  protected $allowed = array('id', 'date_from', 'date_to', 'unit_amount', 'quantity', 'is_taxed', 'tax_amount', 'tax_rate', 'amount', 'discount_amount', 'item_level_discount_amount', 'description', 'entity_type', 'tax_exempt_reason', 'entity_id');
+
+}
+
+?>

+ 86 - 0
upgrade/files/9.0.0.B3-9.0.0.B4/files/plugins/chargebee/ChargeBee/Models/Customer.php

@@ -0,0 +1,86 @@
+<?php
+
+class ChargeBee_Customer extends ChargeBee_Model
+{
+
+  protected $allowed = array('id', 'firstName', 'lastName', 'email', 'phone', 'company', 'vatNumber', 'autoCollection',
+'netTermDays', 'allowDirectDebit', 'createdAt', 'createdFromIp', 'taxability', 'entityCode','exemptNumber', 'resourceVersion', 'updatedAt', 'locale', 'cardStatus', 'fraudFlag', 'billingAddress','contacts', 'paymentMethod', 'invoiceNotes', 'preferredCurrencyCode', 'promotionalCredits','refundableCredits', 'excessPayments', 'metaData', 'deleted');
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers"), $params, $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::sendListRequest(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("customers"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::GET, ChargeBee_Util::encodeURIPath("customers",$id), array(), $env, $headers);
+  }
+
+  public static function update($id, $params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id), $params, $env, $headers);
+  }
+
+  public static function updatePaymentMethod($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"update_payment_method"), $params, $env, $headers);
+  }
+
+  public static function updateBillingInfo($id, $params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"update_billing_info"), $params, $env, $headers);
+  }
+
+  public static function addContact($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"add_contact"), $params, $env, $headers);
+  }
+
+  public static function updateContact($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"update_contact"), $params, $env, $headers);
+  }
+
+  public static function deleteContact($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"delete_contact"), $params, $env, $headers);
+  }
+
+  public static function addPromotionalCredits($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"add_promotional_credits"), $params, $env, $headers);
+  }
+
+  public static function deductPromotionalCredits($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"deduct_promotional_credits"), $params, $env, $headers);
+  }
+
+  public static function setPromotionalCredits($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"set_promotional_credits"), $params, $env, $headers);
+  }
+
+  public static function recordExcessPayment($id, $params, $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"record_excess_payment"), $params, $env, $headers);
+  }
+
+  public static function delete($id, $params = array(), $env = null, $headers = array())
+  {
+    return ChargeBee_Request::send(ChargeBee_Request::POST, ChargeBee_Util::encodeURIPath("customers",$id,"delete"), $params, $env, $headers);
+  }
+
+ }
+
+?>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است