diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/README.md b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/README.md new file mode 100644 index 0000000000..23327c89df --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/README.md @@ -0,0 +1,73 @@ +UNA Platform + + +## Unified Networking Applications + +[![Official Website](https://img.shields.io/badge/website-una.io-blue.svg?style=for-the-badge)](https://unacms.com) +[![GitHub license](https://img.shields.io/github/license/unaio/una?style=for-the-badge)](https://github.com/unacms/una/blob/master/license.txt) +[![Releases](https://img.shields.io/github/downloads/unaio/una/total.svg?style=for-the-badge)](https://github.com/unacms/una) +[![GitHub forks](https://img.shields.io/github/forks/unaio/una?style=for-the-badge)](https://github.com/unacms/una/network) + +UNA is a **Community** Management System. The core platform is a full-stack framework for independent community websites and apps. With UNA content management, collaboration, e-commerce, learning and communication modules - aka **UNA Apps** - you can create communities of all types—from small interest groups to global social media networks. + +![screenshot](https://user-images.githubusercontent.com/22210428/186073113-8f82f8f2-fd5a-4dbb-8328-e0ca847809b9.png) + + + + + + +## Key Features + +* **Unlimited Scalability**: Hundreds, thousands or millions - get as many users as your hosting can handle. UNA won't limit you. +* **Absolute Control**: You own the data, you set the rules, you lead the community. We give you the tools and support. +* **Permissive License**: MIT - the license allowing unrestricted commercial and private use, distribution and modification.. +* **Continious Improvement**: Regular security, performance and feature updates for the core platform, apps and integrations. +* **Collective Innovation**: Improvements and revisions initiated and co-funded by the platform users community. +* **Integrations-Friendly**: Intergate 3rd-party services as UNA apps or use UNA REST API to talk to your external apps. +* **Secure-By-Design**: Full-site SSL encryption, SPD development, regular service updates and vulnurability audits. +* **Compliance Readiness**: We offer HIPAA, GDPR, CCPA (and any other regulations) compliance preparation service. + + + + + +## Documentaion + +To install and run UNA platform, you'll need to a web-hosting server or you can use [UNA Cloud hosting](https://una.io/start) for instant launch. + + +- [UNACMS.COM - Official Website](https://unacms.com) +- [Documentation](https://unacms.com/wiki/Introduction) +- [Discussions](https://unacms.com/page/discussions-home) +- [Contact Team](https://unacms.com/page/contact) +- [Installation](https://unacms.com/wiki/Installation) + + + + +## Download + +You can [download](https://github.com/unaio/una/archive/refs/heads/master.zip) the latest installable version of UNA Core and install to your server. You will need to [create an UNACMS.COM account](https://UNACMS.com) and link it with UNA Studio to install and update UNA Apps. + + + + +## Support & Development + +UNA is managed by [Yasko.Studio](https://yasko.studio) team and developed with the help of [UNA Community](https://una.io). + +* Free community support - [Discussions](https://uunacms.com/page/discussions-home) +* Direct support and professional services - [UNA Pro](https://unacms.com/start) + + +## License + +MIT + +--- + +> Twitter [@unaplatform](https://twitter.com/unaplatform) + +--- +This project is tested with BrowserStack. diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/agents.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/agents.php new file mode 100644 index 0000000000..1505b51419 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/agents.php @@ -0,0 +1,53 @@ +$sAction(); + } + else { + $mixedResponce = $oProvider->call('products/7433953116300.json', ['fields' => 'id,title,handle,body_html,tags,variants'], 'get'); + print_r($mixedResponce); + } +} + +/** + * Work with Assistants + */ +if($sTool == 'asst' && ($iId = bx_get('id')) !== false) { + $oAssistant = BxDolAIAssistant::getObjectInstance((int)$iId); + + if(($sAction = bx_get('a')) !== false) { + $sAction = 'processAction' . bx_gen_method_name(bx_process_input($sAction)); + if(method_exists($oAssistant, $sAction)) + $oAssistant->$sAction(); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/api.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/api.php new file mode 100644 index 0000000000..48b08a3e60 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/api.php @@ -0,0 +1,107 @@ + $sCnf, 'hash' => md5($sCnf)]); + exit(); +} + +// prepare params + +$aRequest = isset($_GET['r']) ? explode('/', $_GET['r']) : []; + +$a = ['sModule', 'sMethod', 'sClass']; +foreach ($aRequest as $i => $v) + if (isset($a[$i]) && preg_match('/^[A-Za-z0-9_-]+$/', $v)) + ${$a[$i]} = $v; + +if (!isset($sClass)) + $sClass = 'Module'; + +foreach ($a as $v) { + if (!isset($$v)) { + header('HTTP/1.0 404 Not Found'); + header('Status: 404 Not Found'); + BxDolLanguages::getInstance(); + echo json_encode(['status' => 404, 'error' => _t("_sys_request_page_not_found_cpt")]); + exit; + } +} + +// check if service exists + +if (!BxDolRequest::serviceExists($sModule, $sMethod, $sClass)) { + header('HTTP/1.0 404 Not Found'); + header('Status: 404 Not Found'); + BxDolLanguages::getInstance(); + echo json_encode(['status' => 404, 'error' => _t("_sys_request_page_not_found_cpt")]); + exit; +} + +// check if service is safe + +if (!getParam('sys_api_access_unsafe_services')) { + $b = BxDolRequest::serviceExists($sModule, 'is_safe_service', 'system' == $sModule ? 'TemplServices' : 'Module'); + $bSafe = $b ? BxDolService::call($sModule, 'is_safe_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : 'Module') : false; + + $bPublic = false; + if (!$bSafe) { + $b = BxDolRequest::serviceExists($sModule, 'is_public_service', 'system' == $sModule ? 'TemplServices' : 'Module'); + $bPublic = $b ? BxDolService::call($sModule, 'is_public_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : 'Module') : false; + } + if (!$bPublic && !$bSafe) { + header('HTTP/1.0 403 Forbidden'); + header('Status: 403 Forbidden'); + echo json_encode(['status' => 403, 'error' => _t("_Access denied")]); + exit; + } +} + +// call service + +if (!($aParams = bx_get('params'))) + $aParams = array(); +elseif (is_string($aParams) && preg_match('/^\[.*\]$/', $aParams)) + $aParams = @json_decode($aParams, true); + +if (!is_array($aParams)) + $aParams = array($aParams); + +$mixedRet = BxDolService::call($sModule, $sMethod, $aParams, $sClass); + +// check output and return JSON + +if (is_array($mixedRet) && isset($mixedRet['error'])) { + header('HTTP/1.0 500 Internal Server Error'); + header('Status: 500 Internal Server Error'); + $a = [ + 'status' => isset($mixedRet['code']) ? $mixedRet['code'] : 500, + 'error' => isset($mixedRet['desc']) ? $mixedRet['desc'] : $mixedRet['error'], + 'data' => isset($mixedRet['data']) ? $mixedRet['data'] : '', + ]; + if (isset($mixedRet['code'])) + $a['code'] = $mixedRet['code']; + echo json_encode($a); + exit; +} + +$aRv = [ + 'status' => 200, + 'module' => $sModule, + 'method' => $sMethod, + 'params' => $aParams, + 'data' => $mixedRet, + 'hash' => md5(getParam('sys_api_config')), +]; + +echo json_encode($aRv); diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/grid.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/grid.php new file mode 100644 index 0000000000..6741531977 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/grid.php @@ -0,0 +1,43 @@ +init(); + +$sAction = 'performAction' . bx_gen_method_name(bx_process_input(bx_get('a'))); +if (method_exists($oGrid, $sAction)) { + if ($oGrid->isActionCsrfCheckingDisabled() || BxDolForm::isCsrfTokenValid(bx_get('csrf_token'), false)) { + $oGrid->$sAction(); + } + else { + echoJson(['msg' => _t('_sys_txt_form_submission_error_csrf_expired'), 'grid' => $oGrid->getCode(false)]); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAI.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAI.php new file mode 100644 index 0000000000..c7e348a83f --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAI.php @@ -0,0 +1,421 @@ +_oDb = new BxDolAIQuery(); + + $this->_iProfileId = (int)getParam('sys_profile_bot'); + + $this->_aExcludeAlertUnits = [ + 'system', 'module_template_method_call' + ]; + + $this->_sCmtsAutomators = 'sys_agents_automators'; + $this->_sCmtsAssistantsChats = 'sys_agents_assistants_chats'; + + $this->_bWriteLog = 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 BxDolAI(); + + return $GLOBALS['bxDolClasses'][__CLASS__]; + } + + public static function callHelper($mixedHelper, $sMessage) + { + $oAI = BxDolAI::getInstance(); + if (is_numeric($mixedHelper)) + $aHelper = $oAI->getHelperById($mixedHelper); + else + $aHelper = $oAI->getHelperByName($mixedHelper); + $oAIModel = $oAI->getModelObject($aHelper['model_id']); + return $oAIModel->getResponseText($aHelper['prompt'], $sMessage); + } + + public static function pruning() + { + BxDolAIAssistant::pruning(); + } + + public static function getDefaultApiKey() + { + return getParam('sys_agents_api_key'); + } + + public static function getDefaultModel() + { + return (int)getParam('sys_agents_model'); + } + + public static function getAssistantForStudio() + { + return ($iId = (int)getParam('sys_agents_studio_assistant')) != 0 ? $iId : 0; + } + + public static function getAssistantForLiveSearch() + { + return ($iId = (int)getParam('sys_agents_live_search_assistant')) != 0 ? $iId : 0; + } + + public static function getAssistantForAskBlock() + { + return ($iId = (int)getParam('sys_agents_ask_block_assistant')) != 0 ? $iId : 0; + } + + public function getProfileId() + { + return $this->_iProfileId; + } + + public function getModels($aParams = []) + { + $aParamsDb = ['sample' => 'all_pairs']; + if(isset($aParams['active'])) + $aParamsDb['active'] = $aParams['active'] === true ? 1 : 0; + if(isset($aParams['hidden'])) + $aParamsDb['hidden'] = $aParams['hidden'] === true ? 1 : 0; + + return $aModel = $this->_oDb->getModelsBy($aParamsDb); + } + + public function getModel($iId) + { + $aModel = $this->_oDb->getModelsBy(['sample' => 'id', 'id' => $iId]); + if(!empty($aModel['params'])) + $aModel['params'] = json_decode($aModel['params'], true); + + return $aModel; + } + + public function getModelObject($iId) + { + if(!$iId) + $iId = $this->getDefaultModel(); + if(!$iId) + return false; + + return BxDolAIModel::getObjectInstance($iId); + } + + public function getProviderObject($iId) + { + if(!$iId) + return false; + + return BxDolAIProvider::getObjectInstance($iId); + } + + public function getAssistants($aParams = []) + { + $aParamsDb = ['sample' => 'all_pairs']; + if(isset($aParams['active'])) + $aParamsDb['active'] = $aParams['active'] === true ? 1 : 0; + if(isset($aParams['hidden'])) + $aParamsDb['hidden'] = $aParams['hidden'] === true ? 1 : 0; + + return $aModel = $this->_oDb->getAssistantsBy($aParamsDb); + } + + public function getAssistantById($iId) + { + return $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $iId]); + } + + public function getAssistantByName($sName) + { + return $this->_oDb->getAssistantsBy(['sample' => 'name', 'name' => $sName]); + } + + public function getAssistantChatById($iId) + { + return $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $iId]); + } + + public function getAssistantChatsTransient($iLifetime = 0) + { + return $this->_oDb->getChatsBy(['sample' => 'type', 'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT, 'lifetime' => $iLifetime]); + } + + public function updateAssistantChatById($iId, $aSet) + { + return $this->_oDb->updateChats($aSet, ['id' => $iId]); + } + + public function getAssistantChatCmts() + { + return $this->_sCmtsAssistantsChats; + } + + public function getAssistantChatCmtsObject($iId, $oTemplate = false) + { + $oCmts = BxDolCmts::getObjectInstance($this->_sCmtsAssistantsChats, (int)$iId, true, $oTemplate); + if(!$oCmts || !$oCmts->isEnabled()) + return false; + + return $oCmts; + } + + public function getHelperById($iId) + { + return $this->_oDb->getHelpersBy(['sample' => 'id', 'id' => $iId]); + } + + public function getHelperByName($sName) + { + return $this->_oDb->getHelpersBy(['sample' => 'name', 'name' => $sName]); + } + + public function getAutomator($iId, $bFullInfo = false) + { + $aAutomator = $this->_oDb->getAutomatorsBy(['sample' => 'id' . ($bFullInfo ? '_full' : ''), 'id' => $iId]); + if(!empty($aAutomator['params'])) + $aAutomator['params'] = json_decode($aAutomator['params'], true); + if($bFullInfo && !empty($aAutomator['model_params'])) + $aAutomator['model_params'] = json_decode($aAutomator['model_params'], true); + + return $aAutomator; + } + + public function getAutomatorInstruction($sType, $mixedParams = false) + { + $mixedResult = ''; + + switch($sType) { + case 'profile': + $mixedResult = "\n ProfileId for system actions = " . $mixedParams; + break; + + case 'providers': + $aProviders = $this->_oDb->getProvidersBy(['sample' => 'ids', 'ids' => $mixedParams]); + if(!empty($aProviders) && is_array($aProviders)) { + $mixedResult = "\n Proividers list = ["; + foreach($aProviders as $aProvider) + $mixedResult .= "\n {'ProviderName' => '" . $aProvider['name'] . "', 'ProviderType' => '" . $aProvider['type_name'] . "'}"; + $mixedResult .= "\n ]"; + } + break; + + case 'helpers': + $aHelpers = $this->_oDb->getHelpersBy(['sample' => 'ids', 'ids' => $mixedParams]); + if(!empty($aHelpers) && is_array($aHelpers)) { + $mixedResult = "\n Helpers list = ["; + foreach($aHelpers as $aHelper) + $mixedResult .= "\n {'" . $aHelper['name'] . "', 'HelperDescription' => '" . $aHelper['description'] . "'}"; + $mixedResult .= "\n ]"; + } + break; + + case 'assistants': + $aAssistants = $this->_oDb->getAssistantsBy(['sample' => 'ids', 'ids' => $mixedParams]); + if(!empty($aAssistants) && is_array($aAssistants)) { + $mixedResult = "\n Assistants list = ["; + foreach($aAssistants as $aAssistant) + $mixedResult .= "\n {'" . $aAssistant['name'] . "', 'AssistantDescription' => '" . $aAssistant['description'] . "'}"; + $mixedResult .= "\n ]"; + } + break; + } + + return $mixedResult; + } + + public function getAutomatorCmts() + { + return $this->_sCmtsAutomators; + } + + public function getAutomatorCmtsObject($iId, $oTemplate = false) + { + $oCmts = BxDolCmts::getObjectInstance($this->_sCmtsAutomators, (int)$iId, true, $oTemplate); + if(!$oCmts || !$oCmts->isEnabled()) + return false; + + return $oCmts; + } + + public function getAutomatorsEvent($sUnit, $sAction) + { + if(in_array($sUnit, $this->_aExcludeAlertUnits)) + return []; + + return $this->_oDb->getAutomatorsBy([ + 'sample' => 'events', + 'alert_unit' => $sUnit, + 'alert_action' => $sAction, + 'active' => true + ]); + } + + public function getAutomatorsScheduler() + { + $aAutomators = $this->_oDb->getAutomatorsBy(['sample' => 'schedulers', 'active' => true]); + foreach($aAutomators as &$aAutomator) + if(!empty($aAutomator['params'])) + $aAutomator['params'] = json_decode($aAutomator['params'], true); + + return $aAutomators; + } + + public function getAutomatorsWebhook($iProviderId) + { + $aAutomators = $this->_oDb->getAutomatorsBy(['sample' => 'webhooks', 'provider_id' => $iProviderId, 'active' => true]); + foreach($aAutomators as &$aAutomator) + if(!empty($aAutomator['params'])) + $aAutomator['params'] = json_decode($aAutomator['params'], true); + + return $aAutomators; + } + + public function callAutomator($sType, $aParams = []) + { + $sMethod = '_callAutomator' . bx_gen_method_name($sType); + if(!method_exists($this, $sMethod)) + return false; + + return $this->$sMethod($aParams); + } + + protected function _callAutomatorEvent($aParams = []) + { + if(!isset($aParams['automator'], $aParams['alert']) || !is_a($aParams['alert'], 'BxDolAlerts')) + return false; + + $oAlert = &$aParams['alert']; + + $this->evalCode($aParams['automator'], ['alert' => $oAlert]); + } + + protected function _callAutomatorScheduler($aParams = []) + { + if(!isset($aParams['automator'])) + return false; + + $this->evalCode($aParams['automator']); + } + + protected function _callAutomatorWebhook($aParams = []) + { + if(!isset($aParams['automator'])) + return false; + + $this->evalCode($aParams['automator']); + } + + public function evalCode($aAutomator, $aParams = []) + { + try { + $this->_evalCode($aAutomator, $aParams); + } + catch (Exception $oException) { + $this->log($oException->getFile() . ':' . $oException->getLine() . ' ' . $oException->getMessage()); + } + catch (Error $oError) { + $this->log($oError->getFile() . ':' . $oError->getLine() . ' ' . $oError->getMessage()); + } + } + + public function emulCode($aAutomator, $aParams = []) + { + ob_start(); + + try { + $this->_evalCode($aAutomator, $aParams); + } + catch (Exception $oException) { + return $oException->getMessage(); + } + catch (Error $oError) { + return $oError->getMessage(); + } + finally { + $sOutput = ob_get_clean(); + + if(!empty($sOutput)) + return $sOutput; + } + } + + public function log($mixedContents, $sSection = '') + { + if(!$this->_bWriteLog) + return; + + if(is_array($mixedContents)) + $mixedContents = var_export($mixedContents, true); + else if(is_object($mixedContents)) + $mixedContents = json_encode($mixedContents); + + if(empty($sSection)) + $sSection = "Core"; + + bx_log('sys_agents', ":\n[" . $sSection . "] " . $mixedContents); + } + + protected function _evalCode($aAutomator, $aParams = []) + { + $sCode = ''; + switch($aAutomator['type']) { + case BX_DOL_AI_AUTOMATOR_EVENT: + $sCode = $aAutomator['code']. '; onAlert($aParams["alert"]->iObject , $aParams["alert"]->iSender , $aParams["alert"]->aExtras);'; + break; + + case BX_DOL_AI_AUTOMATOR_SCHEDULER: + $sCode = $aAutomator['code'] . '; onCron();'; + break; + + case BX_DOL_AI_AUTOMATOR_WEBHOOK: + $sCode = $aAutomator['code'] . '; onHook();'; + break; + } + + eval($sCode); + } +} \ No newline at end of file diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIAssistant.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIAssistant.php new file mode 100644 index 0000000000..07d12a1284 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIAssistant.php @@ -0,0 +1,350 @@ +_log("Unexpected value provided for the credentials"); + + $this->_oDb = new BxDolAIQuery(); + + $this->_iId = (int)$aAssistant['id']; + $this->_aData = $aAssistant; + } + + /** + * Get assistant object instance by ID + * @param $iId assistant ID + * @return object instance or false on error + */ + public static function getObjectInstance($iId) + { + $sPrefix = 'BxDolAIAssistant!'; + + if(isset($GLOBALS['bxDolClasses'][$sPrefix . $iId])) + return $GLOBALS['bxDolClasses'][$sPrefix . $iId]; + + $aAssistant = BxDolAIQuery::getAssistantObject($iId); + if(!$aAssistant) + return false; + + $o = new BxDolAIAssistant($aAssistant); + return ($GLOBALS['bxDolClasses'][$sPrefix . $iId] = $o); + } + + public static function getName($sName) + { + return uriGenerate($sName, 'sys_agents_assistants', 'name', ['lowercase' => false]); + } + + public static function getChatName($sName) + { + return uriGenerate($sName, 'sys_agents_assistants_chats', 'name', ['lowercase' => false]); + } + + public static function pruning() + { + $oAi = BxDolAI::getInstance(); + + if(getParam('sys_agents_asst_chats_trans_del') == 'on') { + $aChats = $oAi->getAssistantChatsTransient(3600); + foreach($aChats as $aChat) + if(!empty($aChat['assistant_id']) && ($oAssistant = self::getObjectInstance($aChat['assistant_id'])) !== false) + $oAssistant->deleteChat($aChat); + } + } + + public function getModelObject() + { + return BxDolAI::getInstance()->getModelObject($this->_aData['model_id']); + } + + public function getChatCmtsObject($iChatId) + { + return BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId); + } + + public function getAskButton($sText) + { + $sTitle = _t('_sys_agents_assistants_txt_ask'); + if(!empty($sText)) + $sTitle .= ': ' . strmaxtextlen($sText, 16); + + return BxDolTemplate::getInstance()->parseButton($sTitle, [ + 'class' => 'bx-btn sys-agents-ask', + 'onclick' => "javascrip:bx_agents_action(this, 'asst', 'ask', {id: " . $this->_iId . ", text: '" . $sText . "'})" + ]); + } + + public function getAskChat($sName = '', $sText = '', $oTemplate = false) + { + if(!$oTemplate) + $oTemplate = BxDolTemplate::getInstance(); + + $bName = !empty($sName); + $bText = !empty($sText); + + $iChatId = 0; + if($bName) { + $aChat = $this->_oDb->getChatsBy(['sample' => 'name', 'name' => $sName]); + if(!empty($aChat) && is_array($aChat)) + $iChatId = (int)$aChat['id']; + } + + if(empty($iChatId)) { + if(!$bName) + $sName = self::getChatName($bText ? strmaxtextlen($sText, 8) : genRndPwd()); + + $iChatId = $this->_oDb->insertChat([ + 'name' => $sName, + 'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT, + 'assistant_id' => $this->_iId, + 'added' => time(), + ]); + } + + $sResult = ''; + if($iChatId !== false && ($oCmts = BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId, $oTemplate)) !== false) { + $oCmts->setAllowDelete(false); + + if(!empty($sText)) + $oCmts->add([ + 'cmt_author_id' => bx_get_logged_profile_id(), + 'cmt_parent_id' => 0, + 'cmt_text' => $sText + ]); + + $sResult = $oCmts->getCommentsBlock([], ['mode' => 'compact']); + } + + return $sResult; + } + + public function getAskBlock($aParams = []) + { + return $this->getAskChat(); + } + + public function deleteChat($mixedChat) + { + $aChat = is_array($mixedChat) ? $mixedChat : $this->_oDb->getChatsBy(['sample' => 'id', 'id' => (int)$mixedChat]); + if(empty($aChat) || !is_array($aChat)) + return false; + + if(($oCmts = $this->getChatCmtsObject($aChat['id'])) !== false) + $oCmts->onObjectDelete(); + + if(!empty($aChat['ai_file_id'])) { + $oAIModel = $this->getModelObject(); + $oAIModel->callVectorStoresFilesDelete($this->_aData['ai_vs_id'], $aChat['ai_file_id']); + $oAIModel->callFilesDelete($aChat['ai_file_id']); + } + + return $this->_oDb->deleteChats(['id' => $aChat['id']]); + } + + public function processActionAddKnowledge() + { + $sAction = 'add_knowledge'; + $oTemplate = BxDolTemplate::getInstance(); + + $aForm = $this->_getForm($sAction, ['id' => $this->_iId]); + $oForm = new BxTemplFormView($aForm); + $oForm->initChecker(); + + if($oForm->isSubmittedAndValid()) { + $oAIModel = $this->getModelObject(); + + $sType = $oForm->getCleanValue('type'); + + $sFile = self::${'_sFile' . ucfirst($sType)}; + $aFile = $this->_oDb->getFilesBy(['sample' => 'assistant_id', 'assistant_id' => $this->_iId, 'name' => $sFile]); + + $aContent = []; + if(!empty($aFile) && is_array($aFile) && !empty($aFile['ai_file_id'])) { + $sContent = $oAIModel->callFilesRetrieveContent($aFile['ai_file_id']); + $aContent = json_decode($sContent, true); + + if(!$oAIModel->callVectorStoresFilesDelete($this->_aData['ai_vs_id'], $aFile['ai_file_id'])) + return echoJson([]); + + if(!$oAIModel->callFilesDelete($aFile['ai_file_id'])) + return echoJson([]); + + $this->_oDb->deleteFiles(['id' => $aFile['id']]); + } + + switch($sType) { + case 'text': + $aContent[] = bx_process_input($oForm->getCleanValue('text')); + break; + + case 'faq': + $aContent[] = [ + 'question' => bx_process_input($oForm->getCleanValue('faq_q')), + 'answer' => bx_process_input($oForm->getCleanValue('faq_a')) + ]; + break; + } + + $aFileResponse = $oAIModel->callFiles(['content' => json_encode($aContent), 'name' => $sFile, 'mime' => 'application/json']); + if(!$aFileResponse) + return echoJson([]); + + if(!$oAIModel->callVectorStoresFiles($this->_aData['ai_vs_id'], ['file_id' => $aFileResponse['id']])) + return echoJson([]); + + $this->_oDb->insertFile([ + 'name' => $sFile, + 'assistant_id' => $this->_iId, + 'added' => time(), + 'ai_file_id' => $aFileResponse['id'], + 'ai_file_size' => $aFileResponse['bytes'] + ]); + + return echoJson(['msg' => _t('_sys_agents_assistants_msg_knowledge_added')]); + } + + $sFormId = $oForm->getId(); + $sContent = BxTemplFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_popup_add_knowledge'), $oTemplate->parseHtmlByName('agents_form.html', [ + 'form_id' => $sFormId, + 'form' => $oForm->getCode(true), + ])); + + return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]); + } + + public function processActionAsk() + { + $sAction = 'ask'; + $oTemplate = BxDolTemplate::getInstance(); + + $sText = bx_get('text'); + if($sText !== false) + $sText = bx_process_input($sText); + + $iChatId = $this->_oDb->insertChat([ + 'name' => self::getChatName(strmaxtextlen($sText, 8)), + 'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT, + 'assistant_id' => $this->_iId, + 'added' => time(), + ]); + + $sPopupContent = ''; + if($iChatId !== false && ($oCmts = BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId, $oTemplate)) !== false) { + $oCmts->setAllowDelete(false); + $oCmts->add([ + 'cmt_author_id' => bx_get_logged_profile_id(), + 'cmt_parent_id' => 0, + 'cmt_text' => $sText + ]); + + $sPopupContent = $oCmts->getCommentsBlock([], ['dynamic_mode' => true]); + } + + $sPopupId = 'bx_agents_assistants_ask'; + $sPopupContent = BxTemplFunctions::getInstance()->popupBox($sPopupId, _t('_sys_agents_assistants_popup_ask'), $oTemplate->parseHtmlByName('agents_popup.html', [ + 'content' => $sPopupContent + ])); + + return echoJson(['popup' => ['html' => $sPopupContent, 'options' => ['closeOnOuterClick' => false]]]); + } + + protected function _getForm($sAction, $aAssistant = []) + { + $aForm = [ + 'form_attrs' => [ + 'id' => 'bx_agents_assistants_' . $sAction, + 'action' => BX_DOL_URL_ROOT . bx_append_url_params('agents.php', ['t' => 'asst', 'a' => $sAction]), + 'method' => 'post', + ], + 'params' => [ + 'db' => [ + 'submit_name' => 'do_submit', + ], + ], + 'inputs' => [], + ]; + + switch($sAction) { + case 'add_knowledge'; + $aForm['inputs'] = [ + 'id' => [ + 'type' => 'hidden', + 'name' => 'id', + 'value' => $aAssistant['id'], + ], + 'type' => [ + 'type' => 'select', + 'name' => 'type', + 'caption' => _t('_sys_agents_assistants_field_kwg_type'), + 'value' => '', + 'values' => [ + ['key' => 'text', 'value' => _t('_sys_agents_assistants_field_kwg_type_text')], + ['key' => 'faq', 'value' => _t('_sys_agents_assistants_field_kwg_type_faq')] + ], + 'attrs' => ['onchange' => "javascript:bx_aa_ak_type_change(this)"], + ], + 'text' => [ + 'type' => 'textarea', + 'name' => 'text', + 'caption' => _t('_sys_agents_assistants_field_kwg_text'), + 'value' => '', + 'tr_attrs' => ['df' => '1', 'dt' => 'text'] + ], + 'faq_q' => [ + 'type' => 'text', + 'name' => 'faq_q', + 'caption' => _t('_sys_agents_assistants_field_kwg_faq_q'), + 'value' => '', + 'tr_attrs' => ['df' => '1', 'dt' => 'faq', 'style' => 'display:none'] + ], + 'faq_a' => [ + 'type' => 'textarea', + 'name' => 'faq_a', + 'caption' => _t('_sys_agents_assistants_field_kwg_faq_a'), + 'value' => '', + 'tr_attrs' => ['df' => '1', 'dt' => 'faq', 'style' => 'display:none'] + ], + 'submit' => [ + 'type' => 'input_set', + 0 => [ + 'type' => 'submit', + 'name' => 'do_submit', + 'value' => _t('_sys_submit'), + ], + 1 => [ + 'type' => 'reset', + 'name' => 'close', + 'value' => _t('_sys_close'), + 'attrs' => ['class' => 'bx-def-margin-sec-left', 'onclick' => '$(\'.bx-popup-applied:visible\').dolPopupHide();'], + ], + ], + ]; + break; + } + + return $aForm; + } +} diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModel.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModel.php new file mode 100644 index 0000000000..1cd735313e --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModel.php @@ -0,0 +1,104 @@ +_sName) != 0) + $this->_log("Unexpected value provided for the credentials"); + + $this->_oDb = new BxDolAIQuery(); + + $this->_iId = (int)$aModel['id']; + $this->_sName = $aModel['name']; + $this->_sCaption = _t($aModel['title']); + $this->_sKey = !empty($aModel['key']) ? $aModel['key'] : BxDolAI::getDefaultApiKey(); + $this->_aParams = !empty($aModel['params']) ? json_decode($aModel['params'], true) : []; + } + + /** + * Get model object instance by model name + * @param $sName model name + * @return object instance or false on error + */ + public static function getObjectInstance($iId) + { + $sPrefix = 'BxDolAIModel!'; + + if(isset($GLOBALS['bxDolClasses'][$sPrefix . $iId])) + return $GLOBALS['bxDolClasses'][$sPrefix . $iId]; + + $aModel = BxDolAIQuery::getModelObject($iId); + if(!$aModel || !is_array($aModel)) + return false; + + $sClass = 'BxDolAIModel'; + if(!empty($aModel['class_name'])) { + $sClass = $aModel['class_name']; + if(!empty($aModel['class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aModel['class_file']); + } + + $o = new $sClass($aModel); + return ($GLOBALS['bxDolClasses'][$sPrefix . $iId] = $o); + } + + public function getParams() + { + return $this->_aParams; + } + + public function setParams($aParams) + { + if(empty($aParams) || !is_array($aParams)) + return; + + $this->_aParams = array_merge($this->_aParams, $aParams); + } + + public function getResponseInit($sType, $aMessage, $aParams = []) + { + // Should be overwritten to get init call response. + } + + public function getResponse($sType, $aMessage, $aParams = []) + { + // Should be overwritten to get call response. + } + + /** + * Internal methods. + */ + protected function _log($mixedError, $bUseLog = true) + { + if(!$bUseLog) { + $sMessage = 'Error occurred'; + if(is_string($mixedError)) + $sMessage = $mixedError; + else if(is_array($mixedError) && isset($mixedError['message'])) + $sMessage = $mixedError['message']; + + throw new Exception($sMessage); + } + else + BxDolAI::getInstance()->log($mixedError, 'Models'); + + return false; + } +} diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModelGpt40.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModelGpt40.php new file mode 100644 index 0000000000..7e943f67f1 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIModelGpt40.php @@ -0,0 +1,480 @@ +_sName = self::$NAME; + + parent::__construct($aModel); + + $this->_sEndpoint = 'https://api.openai.com/v1/threads'; + $this->_sEndpointRuns = $this->_sEndpoint . '/%s/runs'; + $this->_sEndpointRunsCheck = $this->_sEndpoint . '/%s/runs/%s'; + $this->_sEndpointMessages = $this->_sEndpoint . '/%s/messages'; + + $this->_sEndpointAssistants = 'https://api.openai.com/v1/assistants'; + $this->_sEndpointAssistantsDelete = $this->_sEndpointAssistants . '/%s'; + + $this->_sEndpointFiles = 'https://api.openai.com/v1/files'; + $this->_sEndpointFilesRetrieve = $this->_sEndpointFiles . '/%s'; + $this->_sEndpointFilesRetrieveContent = $this->_sEndpointFiles . '/%s/content'; + $this->_sEndpointFilesDelete = $this->_sEndpointFilesRetrieve; + + $this->_sEndpointVectorStores = 'https://api.openai.com/v1/vector_stores'; + $this->_sEndpointVectorStoresDelete = $this->_sEndpointVectorStores . '/%s'; + + $this->_sEndpointVectorStoresFiles = $this->_sEndpointVectorStores . '/%s/files'; + $this->_sEndpointVectorStoresFilesRetrieve = $this->_sEndpointVectorStoresFiles . '/%s'; + $this->_sEndpointVectorStoresFilesDelete = $this->_sEndpointVectorStoresFiles . '/%s'; + + $this->_sEndpointChat = 'https://api.openai.com/v1/chat/completions'; + } + + public function getResponseText($sPrompt, $sMessage) + { + $aMessages = [ + ['role' => 'system', 'content' => $sPrompt], + ['role' => 'user', 'content' => $sMessage] + ]; + + $sResponse = $this->callChat($aMessages); + if($sResponse == 'false') + return false; + + return $sResponse; + } + + public function getResponseInit($sType, $sMessage, $aParams = []) + { + $aResponse = $this->call(['messages' => [['role' => 'user', 'content' => $sMessage]]]); + if(!isset($aResponse['id'], $aResponse['object']) || $aResponse['object'] != 'thread') + return false; + + $sThreadId = $aResponse['id']; + $sAssistantId = isset($aParams['assistant_id']) ? $aParams['assistant_id'] : $this->_getAssistantId($sType . '_init'); + + if(!$this->callRuns($sThreadId, ['assistant_id' => $sAssistantId])) + return false; + + $sResponse = $this->getMessages($sThreadId); + + $mixedResult = []; + switch($sType) { + case BX_DOL_AI_AUTOMATOR_EVENT: + $aResponse = json_decode($sResponse, true); + + $mixedResult = [ + 'alert_unit' => $aResponse['alert_unit'], + 'alert_action' => $aResponse['alert_action'], + 'params' => [ + 'thread_id' => $sThreadId, + 'trigger' => $aResponse['trigger'] + ] + ]; + break; + + case BX_DOL_AI_AUTOMATOR_SCHEDULER: + $mixedResult = [ + 'params' => [ + 'thread_id' => $sThreadId, + 'scheduler_time' => $sResponse + ] + ]; + break; + + case BX_DOL_AI_AUTOMATOR_WEBHOOK: + case BX_DOL_AI_ASSISTANT: + $mixedResult = [ + 'params' => [ + 'thread_id' => $sThreadId, + ] + ]; + break; + } + + return $mixedResult; + } + + public function getResponse($sType, $sMessage, $aParams = []) + { + if(empty($aParams['thread_id'])) + return false; + + if(is_array($sMessage)) + $sMessage = end($sMessage)['content']; + + $sThreadId = $aParams['thread_id']; + if(!$this->callMessages($sThreadId, ['role' => 'user', 'content' => $sMessage])) + return false; + + $sAssistantId = isset($aParams['assistant_id']) ? $aParams['assistant_id'] : $this->_getAssistantId($sType); + if(!$this->callRuns($sThreadId, ['assistant_id' => $sAssistantId])) + return false; + + return $this->getMessages($sThreadId); + } + + public function getAssistant($aParams = []) + { + $aResponseVs = $this->callVectorStores(['name' => $aParams['name']]); + if($aResponseVs === false) + return false; + + $sVectorStoreId = $aResponseVs['id']; + + $aResponseAsst = $this->callAssistants([ + 'model' => $this->_sName, + 'name' => $aParams['name'], + 'instructions' => $aParams['prompt'], + 'tools' => [ + ['type' => 'file_search'] + ], + 'tool_resources' => [ + 'file_search' => [ + 'vector_store_ids' => [$sVectorStoreId] + ] + ] + ]); + if($aResponseAsst === false) + return false; + + $sAssistantId = $aResponseAsst['id']; + + return [ + 'vector_store_id' => $sVectorStoreId, + 'assistant_id' => $sAssistantId + ]; + } + + public function call($aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call']) && is_array($this->_aParams['call'])) + $aData = array_merge($aData, $this->_aParams['call']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + return $this->_call($this->_sEndpoint, $aData); + + } + + public function callRuns($sThreadId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_runs']) && is_array($this->_aParams['call_runs'])) + $aData = array_merge($aData, $this->_aParams['call_runs']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $aResponse = $this->_call(sprintf($this->_sEndpointRuns, $sThreadId), $aData); + if($aResponse !== false && isset($aResponse['id'])) { + $sRunId = $aResponse['id']; + + while($aResponse['status'] != 'completed') { + sleep(2); + + $aResponse = $this->_call(sprintf($this->_sEndpointRunsCheck, $sThreadId, $sRunId), [], 'get'); + } + } + + return $aResponse; + } + + public function callMessages($sThreadId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_messages']) && is_array($this->_aParams['call_messages'])) + $aData = array_merge($aData, $this->_aParams['call_messages']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointMessages, $sThreadId), $aData); + if($mixedResponse !== false) + $mixedResponse = $mixedResponse['content'][0]['text']['value']; + + return $mixedResponse; + } + + /** + * Create a vector store. + * + * @param type $aParams - should have 'name' + * @return boolean + */ + public function callVectorStores($aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_vs']) && is_array($this->_aParams['call_vs'])) + $aData = array_merge($aData, $this->_aParams['call_vs']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call($this->_sEndpointVectorStores, $aData); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'vector_store') + return false; + + return $mixedResponse; + } + + public function callVectorStoresDelete($sVectorStoreId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_vs_delete']) && is_array($this->_aParams['call_vs_delete'])) + $aData = array_merge($aData, $this->_aParams['call_vs_delete']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresDelete, $sVectorStoreId), [], 'DELETE'); + if(empty($mixedResponse) || !is_array($mixedResponse) || !$mixedResponse['deleted']) + return false; + + return $mixedResponse; + } + + /** + * Create a vector store file by attaching a File to a vector store. + * + * @param type $sVectorStoreId + * @param type $aParams - should have 'file_id' + * @return boolean + */ + public function callVectorStoresFiles($sVectorStoreId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_vs_files']) && is_array($this->_aParams['call_vs_files'])) + $aData = array_merge($aData, $this->_aParams['call_vs_files']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresFiles, $sVectorStoreId), $aData); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'vector_store.file') + return false; + + return $mixedResponse; + } + + public function callVectorStoresFilesList($sVectorStoreId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_vs_flist']) && is_array($this->_aParams['call_vs_flist'])) + $aData = array_merge($aData, $this->_aParams['call_vs_flist']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresFiles, $sVectorStoreId), $aData, 'get'); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'list') + return false; + + return $mixedResponse['data']; + } + + public function callVectorStoresFilesRetrieveFile($sVectorStoreId, $sFileId) + { + return $this->_call(sprintf($this->_sEndpointVectorStoresFilesRetrieve, $sVectorStoreId, $sFileId), [], 'get'); + } + + public function callVectorStoresFilesDelete($sVectorStoreId, $sFileId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_vs_files_retrieve']) && is_array($this->_aParams['call_vs_files_retrieve'])) + $aData = array_merge($aData, $this->_aParams['call_vs_files_retrieve']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + return $this->_call(sprintf($this->_sEndpointVectorStoresFilesDelete, $sVectorStoreId, $sFileId), [], 'DELETE'); + } + + public function callAssistants($aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_assts']) && is_array($this->_aParams['call_assts'])) + $aData = array_merge($aData, $this->_aParams['call_assts']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call($this->_sEndpointAssistants, $aData); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'assistant') + return false; + + return $mixedResponse; + } + + public function callAssistantsDelete($sAsstId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_assts_delete']) && is_array($this->_aParams['call_assts_delete'])) + $aData = array_merge($aData, $this->_aParams['call_assts_delete']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointAssistantsDelete, $sAsstId), $aData, 'DELETE'); + if(empty($mixedResponse) || !is_array($mixedResponse) || !$mixedResponse['deleted']) + return false; + + return $mixedResponse; + } + + public function callFiles($aFile, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_files']) && is_array($this->_aParams['call_files'])) + $aData = array_merge($aData, $this->_aParams['call_files']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + if(empty($aData['purpose'])) + $aData['purpose'] = 'assistants'; + + $sName = !empty($aFile['name']) ? $aFile['name'] : 'file_' . time() . '.txt'; + $sMime = !empty($aFile['mime']) ? $aFile['mime'] : 'text/plain'; + $aData['file'] = new CURLStringFile($aFile['content'], $sName, $sMime); + + $mixedResponse = $this->_callFiles($this->_sEndpointFiles, $aData); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file') + return false; + + return $mixedResponse; + } + + public function callFilesRetrieve($sFileId) + { + $mixedResponse = $this->_callFiles(sprintf($this->_sEndpointFilesRetrieve, $sFileId), [], 'get'); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file') + return false; + + return $mixedResponse; + } + + public function callFilesRetrieveContent($sFileId) + { + return bx_file_get_contents(sprintf($this->_sEndpointFilesRetrieveContent, $sFileId), [], 'get', [ + "Authorization: Bearer " . $this->_sKey + ]); + } + + public function callFilesDelete($sFileId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['call_files_delete']) && is_array($this->_aParams['call_files_delete'])) + $aData = array_merge($aData, $this->_aParams['call_files_delete']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_callFiles(sprintf($this->_sEndpointFilesDelete, $sFileId), $aData, 'DELETE'); + if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file') + return false; + + return $mixedResponse; + } + + public function callChat($aMessages, $aParams = []) + { + $aData = [ + 'model' => $this->_sName, + 'messages' => $aMessages + ]; + + if(!empty($this->_aParams['call']) && is_array($this->_aParams['call'])) + $aData = array_merge($aData, $this->_aParams['call']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $sResponse = bx_file_get_contents($this->_sEndpointChat, $aData, "post-json", [ + "Authorization: Bearer " . $this->_sKey, + 'Content-Type: application/json', + 'OpenAI-Beta: assistants=v1' + ]); + + $aResponse = json_decode($sResponse, true); + if(isset($aResponse['error'])) { + $this->_log($aResponse['error']); + return 'false'; + } + + return trim(str_replace(['```json', '```php', '```'], '', $aResponse['choices'][0]['message']['content'])); + } + + public function getMessages($sThreadId, $aParams = []) + { + $aData = []; + if(!empty($this->_aParams['get_messages']) && is_array($this->_aParams['get_messages'])) + $aData = array_merge($aData, $this->_aParams['get_messages']); + if(!empty($aParams) && is_array($aParams)) + $aData = array_merge($aData, $aParams); + + $mixedResponse = $this->_call(sprintf($this->_sEndpointMessages, $sThreadId), $aData, "get"); + if($mixedResponse !== false) + $mixedResponse = trim(str_replace(['```json', '```php', '```'], '', $mixedResponse['data'][0]['content'][0]['text']['value'])); + + return $mixedResponse; + } + + protected function _call($sEndpoint, $aData, $sMethod = "post-json") + { + $sResponse = bx_file_get_contents($sEndpoint, $aData, $sMethod, [ + "Authorization: Bearer " . $this->_sKey, + 'Content-Type: application/json', + 'OpenAI-Beta: assistants=v2' + ]); + + $aResponse = json_decode($sResponse, true); + if(isset($aResponse['error'])) { + $this->_log($aResponse['error']); + return false; + } + + return $aResponse; + } + + protected function _callFiles($sEndpoint, $aData, $sMethod = "post-raw") + { + $sResponse = bx_file_get_contents($sEndpoint, $aData, $sMethod, [ + "Authorization: Bearer " . $this->_sKey + ]); + + $aResponse = json_decode($sResponse, true); + if(isset($aResponse['error'])) { + $this->_log($aResponse['error']); + return false; + } + + return $aResponse; + } + + protected function _getAssistantId($sType) + { + return isset($this->_aParams['assistants'][$sType]) ? $this->_aParams['assistants'][$sType] : ''; + } +} diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIProviderShopifyAdmin.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIProviderShopifyAdmin.php new file mode 100644 index 0000000000..76577c5b3c --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIProviderShopifyAdmin.php @@ -0,0 +1,133 @@ + Product + * https://shopify.dev/docs/api/admin-rest/2023-10/resources/product + * + * 4. REST Admin API -> Order + * https://shopify.dev/docs/api/admin-rest/2023-10/resources/order + * + */ +class BxDolAIProviderShopifyAdmin extends BxDolAIProvider +{ + public static $PROVIDER_NAME = 'shopify_admin'; + + protected $_sShopDomain; + protected $_sAccessToken; + + protected $_sEndpoint; + protected $_sStorefront; + + public function __construct($aProvider) + { + $this->_sProviderName = self::$PROVIDER_NAME; + + parent::__construct($aProvider); + + $this->_sShopDomain = $this->getOption('shop_domain'); + $this->_sAccessToken = $this->getOption('access_token'); + + $this->_sEndpoint = "https://{$this->_sShopDomain}/admin/api/2023-10/"; + $this->_sStorefront = "https://{$this->_sShopDomain}/"; + } + + public function getEntry($sId) + { + $sProduct = $this->call('products/' . $sId . '.json', [ + 'fields' => 'id,title,handle,body_html,tags,variants', + ], 'get'); + + if(empty($sProduct)) + return []; + + $aProduct = json_decode($sProduct, true); + if(empty($aProduct) || !is_array($aProduct) || empty($aProduct['product'])) + return []; + + return $aProduct['product']; + } + + public function getOptionWebhookUrl() + { + return bx_append_url_params(BX_DOL_URL_ROOT . 'agents.php', [ + 'p' => $this->_iId, + 'a' => 'webhook' + ]); + } + + public function processActionWebhook() + { + $sData = @file_get_contents('php://input'); + + $this->_log(json_decode($sData, true)); + + $sHmacHeader = isset($_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256']) ? $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] : ''; + if(!$this->_verifyWebhook($sData, $sHmacHeader)) { + http_response_code(401); + return; + } + + $oAi = BxDolAI::getInstance(); + + $aAutomators = $oAi->getAutomatorsWebhook($this->_iId); + foreach($aAutomators as $aAutomator) + $oAi->callAutomator(BX_DOL_AI_AUTOMATOR_WEBHOOK, [ + 'automator' => $aAutomator + ]); + + http_response_code(200); + } + + public function call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = []) + { + $aHeaders[] = 'Content-Type: application/json'; + if(!empty($this->_sAccessToken)) + $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sAccessToken; + else + $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey); + + $sResponse = bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders); + if(empty($sResponse)) + return false; + + $aResponse = json_decode($sResponse, true); + if(empty($aResponse) || !is_array($aResponse)) + return false; + + return $aResponse; + } + + /** + * Internal methods. + */ + protected function _dateI2S($iTimestamp) + { + return date("Y-m-d", $iTimestamp); + } + + protected function _verifyWebhook($sData, $sHmacHeader) + { + $sHmacCalc = base64_encode(hash_hmac('sha256', $sData, $this->getOption('secret_key'), true)); + return hash_equals($sHmacCalc, $sHmacHeader); + } +} diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIQuery.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIQuery.php new file mode 100644 index 0000000000..7b14d62428 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAIQuery.php @@ -0,0 +1,774 @@ +getRow("SELECT * FROM `sys_agents_models` WHERE `id` = :id", ['id' => $iId]); + if(!$aModel || !is_array($aModel)) + return false; + + return $aModel; + } + + static public function getProviderObject($iId) + { + $oDb = BxDolDb::getInstance(); + + $aProvider = $oDb->getRow("SELECT * FROM `sys_agents_providers` WHERE `id` = :id", ['id' => $iId]); + if(!$aProvider || !is_array($aProvider)) + return false; + + // get type + $aProviderType = $oDb->getRow("SELECT * FROM `sys_agents_provider_types` WHERE `id` = :id", ['id' => $aProvider['type_id']]); + if(!$aProviderType || !is_array($aProviderType)) + return false; + + $aProvider['type'] = $aProviderType; + + // get options + $sQuery = "SELECT + `tpo`.`name` AS `name`, + `tpv`.`value` AS `value` + FROM `sys_agents_provider_options` AS `tpo` + LEFT JOIN `sys_agents_providers_values` AS `tpv` ON `tpo`.`id` = `tpv`.`option_id` AND `tpv`.`provider_id` = :provider_id + WHERE 1 ORDER BY `tpo`.`order`"; + + $aProvider['options'] = $oDb->getAllWithKey($sQuery, 'name', [ + 'provider_id' => $iId + ]); + + return $aProvider; + } + + static public function getProviderIdByName($sName) + { + return (int)BxDolDb::getInstance()->getOne("SELECT `id` FROM `sys_agents_providers` WHERE `name`=:name LIMIT 1", [ + 'name' => $sName + ]); + } + + static public function getAssistantObject($iId) + { + $oDb = BxDolDb::getInstance(); + + $aAssistant = $oDb->getRow("SELECT * FROM `sys_agents_assistants` WHERE `id` = :id", ['id' => $iId]); + if(!$aAssistant || !is_array($aAssistant)) + return false; + + return $aAssistant; + } + + public function getModelsBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `id`=:id"; + break; + + case 'all_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'title'; + $aMethod['params'][3] = []; + + if(isset($aParams['for_asst'])) { + $aMethod['params'][3]['for_asst'] = $aParams['for_asst']; + + $sWhereClause .= " AND `for_asst`=:for_asst"; + } + + if(isset($aParams['active'])) { + $aMethod['params'][3]['active'] = $aParams['active']; + + $sWhereClause .= " AND `active`=:active"; + } + + if(isset($aParams['hidden'])) { + $aMethod['params'][3]['hidden'] = $aParams['hidden']; + + $sWhereClause .= " AND `hidden`=:hidden"; + } + break; + } + + $aMethod['params'][0] = "SELECT * + FROM `sys_agents_models` + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function getAutomatorsBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sSelectClause = "`taa`.*"; + $sJoinClause = $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `taa`.`id`=:id"; + break; + + case 'id_full': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sSelectClause .= ", `tam`.`name` AS `model_name`, `tam`.`title` AS `model_title`, `tam`.`key` AS `model_key`, `tam`.`params` AS `model_params`"; + $sJoinClause .= " LEFT JOIN `sys_agents_models` AS `tam` ON `taa`.`model_id`=`tam`.`id`"; + $sWhereClause .= " AND `taa`.`id`=:id"; + break; + + case 'events': + $aMethod['params'][1] = [ + 'type' => BX_DOL_AI_AUTOMATOR_EVENT, + 'alert_unit' => $aParams['alert_unit'], + 'alert_action' => $aParams['alert_action'] + ]; + + $sWhereClause .= " AND `taa`.`type`=:type AND `taa`.`alert_unit`=:alert_unit AND `taa`.`alert_action`=:alert_action"; + + if(isset($aParams['active'])) { + $aMethod['params'][1]['active'] = (int)$aParams['active']; + + $sWhereClause .= " AND `taa`.`active`=:active"; + } + break; + + case 'schedulers': + $aMethod['params'][1] = [ + 'type' => BX_DOL_AI_AUTOMATOR_SCHEDULER, + ]; + + $sWhereClause .= " AND `taa`.`type`=:type"; + + if(isset($aParams['active'])) { + $aMethod['params'][1]['active'] = (int)$aParams['active']; + + $sWhereClause .= " AND `taa`.`active`=:active"; + } + break; + + case 'webhooks': + $aMethod['params'][1] = [ + 'type' => BX_DOL_AI_AUTOMATOR_WEBHOOK, + ]; + + $sWhereClause .= " AND `taa`.`type`=:type"; + + if(isset($aParams['provider_id'])) { + $aMethod['params'][1]['provider_id'] = (int)$aParams['provider_id']; + + $sJoinClause = "INNER JOIN `sys_agents_automators_providers` AS `tap` ON `taa`.`id`=`tap`.`automator_id`"; + $sWhereClause .= " AND `tap`.`provider_id`=:provider_id"; + } + + if(isset($aParams['active'])) { + $aMethod['params'][1]['active'] = (int)$aParams['active']; + + $sWhereClause .= " AND `taa`.`active`=:active"; + } + break; + + case 'providers_by_id_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'provider_id'; + $aMethod['params'][3] = [ + 'id' => $aParams['id'] + ]; + + $sSelectClause = "`taap`.`id`, `taap`.`provider_id`"; + $sJoinClause = "INNER JOIN `sys_agents_automators_providers` AS `taap` ON `taa`.`id`=`taap`.`automator_id`"; + $sWhereClause = " AND `taa`.`id`=:id"; + break; + + case 'helpers_by_id_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'helper_id'; + $aMethod['params'][3] = [ + 'id' => $aParams['id'] + ]; + + $sSelectClause = "`taah`.`id`, `taah`.`helper_id`"; + $sJoinClause = "INNER JOIN `sys_agents_automators_helpers` AS `taah` ON `taa`.`id`=`taah`.`automator_id`"; + $sWhereClause = " AND `taa`.`id`=:id"; + break; + + case 'assistants_by_id_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'assistant_id'; + $aMethod['params'][3] = [ + 'id' => $aParams['id'] + ]; + + $sSelectClause = "`taaa`.`id`, `taaa`.`assistant_id`"; + $sJoinClause = "INNER JOIN `sys_agents_automators_assistants` AS `taaa` ON `taa`.`id`=`taaa`.`automator_id`"; + $sWhereClause = " AND `taa`.`id`=:id"; + break; + } + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_agents_automators` AS `taa` " . $sJoinClause . " + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function updateAutomators($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return false; + + return (int)$this->query("UPDATE `sys_agents_automators` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + public function insertAutomatorProvider($aParamsSet) + { + if(empty($aParamsSet)) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_automators_providers` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false; + } + + public function updateAutomatorProvider($aParamsSet, $aParamsWhere) + { + if(empty($aParamsSet) || empty($aParamsWhere)) + return false; + + return $this->query("UPDATE `sys_agents_automators_providers` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ")); + } + + public function deleteAutomatorProviders($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_automators_providers` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0; + } + + public function deleteAutomatorProvidersById($mixedId) + { + if(!is_array($mixedId)) + $mixedId = [$mixedId]; + + return (int)$this->query("DELETE FROM `sys_agents_automators_providers` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0; + } + + public function insertAutomatorHelper($aParamsSet) + { + if(empty($aParamsSet)) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_automators_helpers` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false; + } + + public function updateAutomatorHelper($aParamsSet, $aParamsWhere) + { + if(empty($aParamsSet) || empty($aParamsWhere)) + return false; + + return $this->query("UPDATE `sys_agents_automators_helpers` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ")); + } + + public function deleteAutomatorHelpers($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_automators_helpers` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0; + } + + public function deleteAutomatorHelpersById($mixedId) + { + if(!is_array($mixedId)) + $mixedId = [$mixedId]; + + return (int)$this->query("DELETE FROM `sys_agents_automators_helpers` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0; + } + + public function insertAutomatorAssistant($aParamsSet) + { + if(empty($aParamsSet)) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_automators_assistants` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false; + } + + public function updateAutomatorAssistant($aParamsSet, $aParamsWhere) + { + if(empty($aParamsSet) || empty($aParamsWhere)) + return false; + + return $this->query("UPDATE `sys_agents_automators_assistants` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ")); + } + + public function deleteAutomatorAssistants($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_automators_assistants` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0; + } + + public function deleteAutomatorAssistantsById($mixedId) + { + if(!is_array($mixedId)) + $mixedId = [$mixedId]; + + return (int)$this->query("DELETE FROM `sys_agents_automators_assistants` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0; + } + + public function getProviderTypesBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `id`=:id"; + break; + + case 'name': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'name' => $aParams['name'] + ]; + + $sWhereClause .= " AND `name`=:name"; + break; + + case 'all_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'title'; + + if(isset($aParams['active'])) { + $aMethod['params'][3] = [ + 'active' => $aParams['active'] + ]; + + $sWhereClause = " AND `active`=:active"; + } + break; + } + + $aMethod['params'][0] = "SELECT * + FROM `sys_agents_provider_types` + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function getProviderOptionsBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `id`=:id"; + break; + + case 'provider_type_id': + $aMethod['params'][1] = [ + 'provider_type_id' => $aParams['provider_type_id'] + ]; + + $sWhereClause .= " AND `provider_type_id`=:provider_type_id"; + break; + } + + $aMethod['params'][0] = "SELECT * + FROM `sys_agents_provider_options` + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function getProvidersBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + + $sSelectClause = "`tp`.*"; + $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = ""; + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause = " AND `tp`.`id`=:id"; + break; + + case 'ids': + $sSelectClause .= ", `tpt`.`name` AS `type_name`"; + $sJoinClause = "INNER JOIN `sys_agents_provider_types` AS `tpt` ON `tp`.`type_id`=`tpt`.`id`"; + $sWhereClause = " AND `tp`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")"; + break; + + case 'options_by_id': + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sSelectClause = "`tpo`.`id`, `tpo`.`name`, `tpo`.`type`, `tpo`.`title`, `tpo`.`description`, `tpv`.`value`"; + $sJoinClause = "LEFT JOIN `sys_agents_provider_options` AS `tpo` ON `tp`.`type_id`=`tpo`.`provider_type_id` LEFT JOIN `sys_agents_providers_values` AS `tpv` ON `tp`.`id`=`tpv`.`provider_id` AND `tpo`.`id`=`tpv`.`option_id`"; + $sWhereClause = " AND `tp`.`id`=:id"; + break; + + case 'all_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + + if(isset($aParams['active'])) { + $aMethod['params'][3] = [ + 'active' => $aParams['active'] + ]; + + $sWhereClause = " AND `tp`.`active`=:active"; + } + break; + } + + $sOrderClause = !empty($sOrderClause) ? "ORDER BY " . $sOrderClause : $sOrderClause; + $sLimitClause = !empty($sLimitClause) ? "LIMIT " . $sLimitClause : $sLimitClause; + + $aMethod['params'][0] = "SELECT + " . $sSelectClause . " + FROM `sys_agents_providers` AS `tp` " . $sJoinClause . " + WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause; + + return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']); + } + + public function insertProviderValue($aParamsSet) + { + if(empty($aParamsSet) || !is_array($aParamsSet) || !isset($aParamsSet['value'])) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_providers_values` SET " . $this->arrayToSQL($aParamsSet) . " ON DUPLICATE KEY UPDATE `value`=:value", [ + 'value' => $aParamsSet['value'] + ]) > 0 ? (int)$this->lastId() : false; + } + + public function updateProviderValue($aParamsSet, $aParamsWhere) + { + if(empty($aParamsSet)) + return false; + + $sWhereClause = "1"; + if(!empty($aParamsWhere) && is_array($aParamsWhere)) + $sWhereClause = $this->arrayToSQL($aParamsWhere, ' AND '); + + return $this->query("UPDATE `sys_agents_providers_values` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $sWhereClause) !== false; + } + + public function deleteProviderValues($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_providers_values` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0; + } + + public function getHelpersBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sSelectClause = "`th`.*"; + $sJoinClause = $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `th`.`id`=:id"; + break; + case 'name': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'name' => $aParams['name'] + ]; + + $sWhereClause .= " AND `th`.`name`=:name"; + break; + case 'ids': + $sWhereClause = " AND `th`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")"; + break; + + case 'all_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + + if(isset($aParams['active'])) { + $aMethod['params'][3] = [ + 'active' => $aParams['active'] + ]; + + $sWhereClause = " AND `th`.`active`=:active"; + } + break; + } + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_agents_helpers` AS `th` " . $sJoinClause . " + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function getAssistantsBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sSelectClause = "`ta`.*"; + $sJoinClause = $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `ta`.`id`=:id"; + break; + + case 'name': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'name' => $aParams['name'] + ]; + + $sWhereClause .= " AND `ta`.`name`=:name"; + break; + + case 'ids': + $sWhereClause = " AND `ta`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")"; + break; + + case 'all_pairs': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + $aMethod['params'][3] = []; + + if(isset($aParams['active'])) { + $aMethod['params'][3]['active'] = $aParams['active']; + + $sWhereClause .= " AND `ta`.`active`=:active"; + } + + if(isset($aParams['hidden'])) { + $aMethod['params'][3]['hidden'] = $aParams['hidden']; + + $sWhereClause .= " AND `ta`.`hidden`=:hidden"; + } + break; + } + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_agents_assistants` AS `ta` " . $sJoinClause . " + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function updateAssistants($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return false; + + return (int)$this->query("UPDATE `sys_agents_assistants` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + public function getChatsBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sSelectClause = "`tac`.*"; + $sJoinClause = $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `tac`.`id`=:id"; + break; + + case 'name': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'name' => $aParams['name'] + ]; + + $sWhereClause .= " AND `tac`.`name`=:name"; + break; + + case 'assistant_id': + $aMethod['params'][1] = [ + 'assistant_id' => $aParams['assistant_id'] + ]; + + $sWhereClause .= " AND `tac`.`assistant_id`=:assistant_id"; + break; + + case 'type': + $aMethod['params'][1] = [ + 'type' => $aParams['type'] + ]; + + $sWhereClause .= " AND `tac`.`type`=:type"; + + if(isset($aParams['lifetime']) && (int)$aParams['lifetime'] > 0) { + $aMethod['params'][1]['lifetime'] = (int)$aParams['lifetime']; + + $sWhereClause .= " AND (UNIX_TIMESTAMP() - `tac`.`added`) >= :lifetime"; + } + break; + } + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_agents_assistants_chats` AS `tac` " . $sJoinClause . " + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function insertChat($aParamsSet) + { + if(empty($aParamsSet) || !is_array($aParamsSet)) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_assistants_chats` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false; + } + + public function updateChats($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return false; + + return (int)$this->query("UPDATE `sys_agents_assistants_chats` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + public function deleteChats($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_assistants_chats` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0; + } + + public function getFilesBy($aParams = []) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sSelectClause = "`taf`.*"; + $sJoinClause = $sWhereClause = ""; + + switch($aParams['sample']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = [ + 'id' => $aParams['id'] + ]; + + $sWhereClause .= " AND `taf`.`id`=:id"; + break; + + case 'assistant_id': + $aMethod['params'][1] = [ + 'assistant_id' => $aParams['assistant_id'] + ]; + + $sWhereClause .= " AND `taf`.`assistant_id`=:assistant_id"; + + if(!empty($aParams['name'])) { + $aMethod['name'] = 'getRow'; + $aMethod['params'][1]['name'] = $aParams['name']; + + $sWhereClause .= " AND `taf`.`name`=:name"; + } + break; + } + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_agents_assistants_files` AS `taf` " . $sJoinClause . " + WHERE 1" . $sWhereClause; + + return call_user_func_array([$this, $aMethod['name']], $aMethod['params']); + } + + public function insertFile($aParamsSet) + { + if(empty($aParamsSet) || !is_array($aParamsSet)) + return false; + + return (int)$this->query("INSERT INTO `sys_agents_assistants_files` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false; + } + + public function updateFiles($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return false; + + return (int)$this->query("UPDATE `sys_agents_assistants_files` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + public function deleteFiles($aParamsWhere) + { + if(empty($aParamsWhere)) + return false; + + return (int)$this->query("DELETE FROM `sys_agents_assistants_files` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAccount.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAccount.php new file mode 100644 index 0000000000..8c5c6b1fa4 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAccount.php @@ -0,0 +1,1075 @@ +_iAccountID = $iAccountId; // since constructor is protected $iAccountId is always valid + $this->_oQuery = BxDolAccountQuery::getInstance(); + } + + /** + * Prevent cloning the instance + */ + public function __clone() + { + $sClass = get_class($this) . '_' . $this->_iProfileID; + if (isset($GLOBALS['bxDolClasses'][$sClass])) + trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); + } + + /** + * Get singleton instance of the class + */ + public static function getInstance($mixedAccountId = false, $bClearCache = false) + { + if (!$mixedAccountId) + $mixedAccountId = getLoggedId(); + + $iAccountId = self::getID($mixedAccountId); + if (!$iAccountId) + return false; + + $sClass = __CLASS__ . '_' . $iAccountId; + + if ($bClearCache && isset($GLOBALS['bxDolClasses'][$sClass])) + unset($GLOBALS['bxDolClasses'][$sClass]); + + if(!isset($GLOBALS['bxDolClasses'][$sClass])) + $GLOBALS['bxDolClasses'][$sClass] = new BxDolAccount($iAccountId); + + return $GLOBALS['bxDolClasses'][$sClass]; + } + + /** + * Get studio operator account singleton instance on the class + */ + public static function getInstanceStudioOperator() + { + $oQuery = BxDolAccountQuery::getInstance(); + if (!($iId = $oQuery->getStudioOperatorId())) + return false; + + return self::getInstance($iId); + } + + /** + * Get account id + */ + public function id() + { + $a = $this->getInfo($this->_iAccountID); + return isset($a['id']) ? $a['id'] : false; + } + + /** + * Check if account is confirmed, it is checked by email confirmation + */ + public function isConfirmed($iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $a = $this->getInfo($iId); + + $bResult = false; + $sConfirmationType = getParam('sys_account_confirmation_type'); + switch($sConfirmationType) { + case BX_ACCOUNT_CONFIRMATION_NONE: + $bResult = true; + break; + + case BX_ACCOUNT_CONFIRMATION_EMAIL: + if($a['email_confirmed']) + $bResult = true; + break; + + case BX_ACCOUNT_CONFIRMATION_PHONE: + if($a['phone_confirmed']) + $bResult = true; + break; + + case BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE: + if($a['email_confirmed'] && $a['phone_confirmed']) + $bResult = true; + break; + + case BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE: + if($a['email_confirmed'] || $a['phone_confirmed']) + $bResult = true; + break; + + } + + /** + * @hooks + * @hookdef hook-account-is_confirmed 'account', 'is_confirmed' - hook in $oAccount->isConfirmed check + * - $unit_name - equals `account` + * - $action - equals `is_confirmed` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `type` - [string] confirmation type can be none/phone/email/email_and_phone/email_or_phone + * - `override_result` - [bool] by ref, if account confirmed = true, otherwise false, can be overridden in hook processing + * @hook @ref hook-account-is_confirmed + */ + bx_alert('account', 'is_confirmed', $iId, false, array('type' => $sConfirmationType, 'override_result' => &$bResult)); + + return $bResult; + } + + public function getCurrentConfirmationStatusValue($iAccountId = false) + { + $a = $this->getInfo((int)$iAccountId); + $sTmp = $a['email_confirmed'] . $a['phone_confirmed']; + switch ($sTmp) { + case '01': + return BX_ACCOUNT_CONFIRMATION_PHONE; + case '10': + return BX_ACCOUNT_CONFIRMATION_EMAIL; + case '11': + return BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE; + } + return BX_ACCOUNT_CONFIRMATION_NONE; + } + + public function isConfirmedEmail($iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $bResult = false; + if(self::isNeedConfirmEmail()) { + $a = $this->getInfo($iId); + + $bResult = $a['email_confirmed'] ? true : false; + } + else + $bResult = true; + + /** + * @hooks + * @hookdef hook-account-is_confirmed 'account', 'is_confirmed_email' - hook in $oAccount->isConfirmedEmail check + * - $unit_name - equals `account` + * - $action - equals `is_confirmed_email` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [bool] by ref, if email confirmed = true, otherwise false, can be overridden in hook processing + * @hook @ref hook-account-is_confirmed_email + */ + bx_alert('account', 'is_confirmed_email', $iId, false, array('override_result' => &$bResult)); + + return $bResult; + } + + public function isConfirmedPhone($iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $bResult = false; + if(self::isNeedConfirmPhone()) { + $a = $this->getInfo((int)$iAccountId); + + $bResult = $a['phone_confirmed'] ? true : false; + } + else + $bResult = true; + + /** + * @hooks + * @hookdef hook-account-is_confirmed_phone 'account', 'is_confirmed_phone' - hook in $oAccount->isConfirmedPhone check + * - $unit_name - equals `account` + * - $action - equals `is_confirmed_phone` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [bool] by ref, if phone confirmed = true, otherwise false, can be overridden in hook processing + * @hook @ref hook-account-is_confirmed_phone + */ + bx_alert('account', 'is_confirmed_phone', $iId, false, array('override_result' => &$bResult)); + + return $bResult; + } + + static public function isNeedConfirmEmail() + { + if(in_array(getParam('sys_account_confirmation_type'), array(BX_ACCOUNT_CONFIRMATION_EMAIL, BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE))) + return true; + return false; + } + + static public function isNeedConfirmPhone() + { + if(in_array(getParam('sys_account_confirmation_type'), array(BX_ACCOUNT_CONFIRMATION_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE))) + return true; + return false; + } + + public function isLocked($iAccountId = false) + { + $a = $this->getInfo((int)$iAccountId); + return $a['locked'] ? true : false; + } + + // + + /** + * Set account email to confirmed or unconfirmed + * @param int $isConfirmed - false: mark email as unconfirmed, true: as confirmed + * @param int $iAccountId - optional account id + * @return true on success or false on error + */ + public function updateEmailConfirmed($isConfirmed, $isAutoSendConfrmationEmail = true, $iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + if (!$isConfirmed && $isAutoSendConfrmationEmail && self::isNeedConfirmEmail()) // if email_confirmation procedure is enabled - send email confirmation letter + $this->sendConfirmationEmail($iId); + + if ($this->_oQuery->updateEmailConfirmed($isConfirmed, $iId)) { + $this->_aInfo = false; + /** + * @hooks + * @hookdef hook-account-confirm 'account', 'confirm' - hook in email confirmation $oAccount->updateEmailConfirmed + * - $unit_name - equals `account` + * - $action - can be confirm/unconfirm + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - not used + * @hook @ref hook-account-confirm + */ + bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId); + return true; + } + return false; + } + + /** + * Set account phone to confirmed or unconfirmed + * @param int $isConfirmed - false: mark phone as unconfirmed, true: as confirmed + * @param int $iAccountId - optional account id + * @return true on success or false on error + */ + public function updatePhoneConfirmed($isConfirmed, $iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + if ($this->_oQuery->updatePhoneConfirmed($isConfirmed, $iId)) { + $this->_aInfo = false; + bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId); + return true; + } + return false; + } + /** + * Change account password + * @param string $sPassword - new password + * @param int $iAccountId - optional account id + * @return true on success or false on error + */ + public function updatePassword($sPassword, $iAccountId = false) + { + $sSalt = genRndSalt(); + $sPasswordHash = encryptUserPwd($sPassword, $sSalt); + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $oAccountSender = BxDolAccount::getInstance(); + + $this->_oQuery->logPassword($iId); + $iPasswordExpired = $this->getPasswordExpiredDateByAccount($iAccountId); + + if((int)$this->_oQuery->updatePassword($sPasswordHash, $sSalt, $iId, $iPasswordExpired) > 0) { + /** + * @hooks + * @hookdef hook-account-edited 'account', 'edited' - hook on account edited $oAccount->updatePassword + * - $unit_name - equals `account` + * - $action - equals edited + * - $object_id - account id + * - $sender_id - account sender id + * - $extra_params - array of additional params with the following array keys: + * - `action` - [string] action's name, can be reset_password + * @hook @ref hook-account-edited + */ + bx_alert('account', 'edited', $iId, $oAccountSender ? $oAccountSender->id() : $iId, array('action' => 'reset_password')); + $this->doAudit($iId, '_sys_audit_action_account_reset_password'); + return true; + } + return false; + } + /** + * Set account phone + * @param string $sPhone - phone number + * @param int $iAccountId - optional account id + * @return true on success or false on error + */ + public function updatePhone($sPhone, $iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + if ($this->_oQuery->updatePhone($sPhone, $iId)) { + /** + * @hooks + * @hookdef hook-account-set_phone 'account', 'set_phone' - hook after accout password changed + * - $unit_name - equals `account` + * - $action - equals `set_phone` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - not used + * @hook @ref hook-account-set_phone + */ + bx_alert('account', 'set_phone', $iId); + return true; + } + return false; + } + /** + * Switch context automatically to the first available profile + * @param $iProfileIdFilterOut profile ID to exclude from the list of possible profiles + * @param $iAccountId account ID to use istead of current account + * @return true on success or false on error + */ + public function updateProfileContextAuto($iProfileIdFilterOut = false, $iAccountId = false) + { + $oAccount = (!$iAccountId || $iAccountId == $this->_iAccountID ? $this : BxDolAccount::getInstance ($iAccountId)); + if (!$oAccount) + return false; + $aAccountInfo = $oAccount->getInfo(); + $aProfiles = $oAccount->getProfiles(); + $oProfileAccount = BxDolProfile::getInstanceAccountProfile($oAccount->id()); + + // unset deleted profile and account profile + if ($iProfileIdFilterOut) + unset($aProfiles[$iProfileIdFilterOut]); + unset($aProfiles[$oProfileAccount->id()]); + + if ($aProfiles) { + // try to use another profile + reset($aProfiles); + $iProfileId = key($aProfiles); + } + else { + // if no profiles exist, use account profile + $iProfileId = $oProfileAccount->id(); + } + + return $oAccount->updateProfileContext($iProfileId); + } + + public function updateProfileContext($iSwitchToProfileId, $iAccountId = false) + { + $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $aInfo = $this->getInfo((int)$iId); + if (!$aInfo) + return false; + + $ret = null; + /** + * @hooks + * @hookdef hook-account-before_switch_context 'account', 'before_switch_context' - hook before switch profile_id frof current logged user + * - $unit_name - equals `account` + * - $action - equals `before_switch_context` + * - $object_id - account id + * - $sender_id - profile_id to switch to + * - $extra_params - array of additional params with the following array keys: + * - `profile_id_current` - [int] current profile_id + * - `override_result` - [int] by ref, profile_id to switch to, can be overridden in hook processing + * @hook @ref hook-account-before_switch_context + */ + bx_alert('account', 'before_switch_context', $iId, $iSwitchToProfileId, array('profile_id_current' => $aInfo['profile_id'], 'override_result' => &$ret)); + if ($ret !== null) + return $ret; + + if (!$this->_oQuery->updateCurrentProfile($iId, $iSwitchToProfileId)) + return false; + + $this->_aInfo = false; + + /** + * @hooks + * @hookdef hook-account-switch_context 'account', 'switch_context' - hook before switch profile_id frof current logged user + * - $unit_name - equals `account` + * - $action - equals `switch_context` + * - $object_id - account id + * - $sender_id - profile_id to switch to + * - $extra_params - array of additional params with the following array keys: + * - `profile_id_old` - [int] old profile_id + * @hook @ref hook-account-switch_context + */ + bx_alert('account', 'switch_context', $iId, $iSwitchToProfileId, array('profile_id_old' => $aInfo['profile_id'])); + + return true; + } + + /** + * Send "confirmation" email + */ + public function sendConfirmationEmail($iAccountId = false) + { + $iAccountId = (int)$iAccountId; + if(!$iAccountId) + $iAccountId = $this->_iAccountID; + + $aAccountInfo = $this->getInfo($iAccountId); + if(empty($aAccountInfo) || !is_array($aAccountInfo)) + return false; + + $oKey = BxDolKey::getInstance(); + $sConfirmationCode = $oKey->getNewKeyNumeric(['account_id' => $iAccountId]); + $sConfirmationLink = bx_append_url_params(BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email'), ['code' => $sConfirmationCode]); + + $sEmailTemplate = 't_Confirmation'; + $aEmailReplaceVars = [ + 'name' => $this->getDisplayName($iAccountId), + 'email' => $aAccountInfo['email'], + 'conf_code' => $sConfirmationCode, + 'conf_link' => $sConfirmationLink, + 'conf_form_link' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email') + ]; + + $bResult = sendMailTemplate($sEmailTemplate, $iAccountId, (int)$aAccountInfo['profile_id'], $aEmailReplaceVars, BX_EMAIL_SYSTEM); + if($bResult) + $this->doAudit($iAccountId, '_sys_audit_action_account_resend_confirmation_email'); + + return $bResult; + } + + public function sendResetPasswordEmail($iAccountId = false) + { + $iAccountId = (int)$iAccountId; + if(!$iAccountId) + $iAccountId = $this->_iAccountID; + + $aAccountInfo = $this->getInfo($iAccountId); + if(empty($aAccountInfo) || !is_array($aAccountInfo)) + return false; + + $sKey = bx_get_reset_password_key($aAccountInfo['email']); + + $aReplaceVars = array( + 'name' => $this->getDisplayName($iAccountId), + 'email' => $aAccountInfo['email'], + 'key' => $sKey, + 'forgot_password_url' => bx_get_reset_password_link_by_key($sKey) + ); + + return sendMailTemplate('t_Forgot', $iAccountId, (int)$aAccountInfo['profile_id'], $aReplaceVars, BX_EMAIL_SYSTEM); + } + + /** + * Get account info + */ + public function getInfo($iAccountId = false) + { + if ($iAccountId && $iAccountId != $this->_iAccountID) + return $this->_oQuery->getInfoById((int)$iAccountId ? (int)$iAccountId : $this->_iAccountID); + + if ($this->_aInfo) + return $this->_aInfo; + + $this->_aInfo = $this->_oQuery->getInfoById($this->_iAccountID); + return $this->_aInfo; + } + + /** + * Get account display name + */ + public function getDisplayName($iAccountId = false) + { + $aInfo = $this->getInfo($iAccountId); + + $sDisplayName = !empty($aInfo['name']) ? $aInfo['name'] : _t('_sys_txt_user_n', $aInfo['id']); + + /** + * @hooks + * @hookdef hook-account-account_name 'account', 'account_name' - hook on get account display name + * - $unit_name - equals `account` + * - $action - equals `account_name` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `info` - [array] contains account info from $oAccount->getInfo() + * - `display_name` - [string] by ref, account display name, can be overridden in hook processing + * @hook @ref hook-account-account_name + */ + bx_alert('account', 'account_name', $iAccountId, 0, array('info' => $aInfo, 'display_name' => &$sDisplayName)); + + return bx_process_output($sDisplayName); + } + + /** + * Get account url + */ + public function getUrl($iAccountId = false) + { + return 'javascript:void(0);'; + } + + /** + * Get account url + */ + public function getUnit($iAccountId = false, $aParams = array()) + { + + $sTemplate = 'unit'; + $sTemplateSize = false; + $aTemplateVars = array(); + if(!empty($aParams['template'])) { + if(is_string($aParams['template'])) + $sTemplate = $aParams['template']; + else if(is_array($aParams['template'])) { + if(!empty($aParams['template']['name'])) + $sTemplate = $aParams['template']['name']; + + if(!empty($aParams['template']['size'])) + $sTemplateSize = $aParams['template']['size']; + + if(!empty($aParams['template']['vars'])) + $aTemplateVars = $aParams['template']['vars']; + } + } + $sTemplate = 'account_' . $sTemplate . '.html'; + if(empty($sTemplateSize)) + $sTemplateSize = 'thumb'; + + $sTitle = $this->getDisplayName($iAccountId); + + $aTmplVars = array( + 'size' => $sTemplateSize, + 'color' => implode(', ', BxDolTemplate::getColorCode(($iAccountId ? $iAccountId : $this->_iAccountID), 1.0)), + 'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1)), + 'content_url' => $this->getUrl($iAccountId), + 'title' => $sTitle, + 'title_attr' => bx_html_attribute($sTitle), + 'bx_if:show_online' => array( + 'condition' => $this->isOnline($iAccountId), + 'content' => array() + ) + ); + + if(!empty($aTemplateVars) && is_array($aTemplateVars)) + $aTmplVars = array_merge ($aTmplVars, $aTemplateVars); + + return BxDolTemplate::getInstance()->parseHtmlByName($sTemplate, $aTmplVars); + } + + /** + * Get picture url + */ + public function getPicture($iAccountId = false) + { + return BxDolTemplate::getInstance()->getImageUrl('account.svg'); + } + + /** + * Get big (2x) avatar url + */ + public function getAvatarBig($iAccountId = false) + { + return BxDolTemplate::getInstance()->getImageUrl('account.svg'); + } + + /** + * Get avatar url + */ + public function getAvatar($iAccountId = false) + { + return BxDolTemplate::getInstance()->getImageUrl('account.svg'); + } + + /** + * Get thumb picture url + */ + public function getThumb($iAccountId = false) + { + return BxDolTemplate::getInstance()->getImageUrl('account.svg'); + } + + /** + * Get icon picture url + */ + public function getIcon($iAccountId = false) + { + return BxDolTemplate::getInstance()->getImageUrl('account.svg'); + } + + /** + * Get account email + */ + public function getEmail($iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $aAccountInfo = $this->getInfo($iAccountId); + return $aAccountInfo['email']; + } + + /** + * Get language ID + */ + public function getLanguageId($iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $aAccountInfo = $this->getInfo($iAccountId); + return $aAccountInfo['lang_id']; + } + + /** + * Is account online + */ + public function isOnline($iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + return $this->_oQuery->isOnline($iAccountId); + } + + /** + * Validate account. + * @param $s - account identifier (id or email) + * @return account id or false if account was not found + */ + static public function getID($s) + { + $oQuery = BxDolAccountQuery::getInstance(); + + bx_import('BxDolForm'); + if (BxDolFormCheckerHelper::checkEmail($s)) { + $iId = (int)$oQuery->getIdByEmail($s); + return $iId ? $iId : false; + } + if (preg_match("/^\+[0-9\s]+$/", $s)) { + $iId = (int)$oQuery->getIdByPhone($s); + return $iId ? $iId : false; + } + + $iId = $oQuery->getIdById((int)$s); + return $iId ? $iId : false; + } + + /** + * Check if profiles limit reached + */ + public function isProfilesLimitReached () + { + $iProfilesLimit = (int)getParam('sys_account_limit_profiles_number'); + /** + * @hooks + * @hookdef hook-account-get_limit_profiles_number 'account', 'get_limit_profiles_number' - hook on get account limit on the number of profiles + * - $unit_name - equals `account` + * - $action - equals `get_limit_profiles_number` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `account_id` - [int] account id + * - `number` - [int] by ref, account limit on the number of profiles, can be overridden in hook processing + * @hook @ref hook-account-get_limit_profiles_number + */ + bx_alert('account', 'get_limit_profiles_number', 0, 0, array('account_id' => $this->_iAccountID, 'number' => &$iProfilesLimit)); + if (!isAdmin() && $iProfilesLimit && ($iProfilesNum = $this->getProfilesNumber()) && $iProfilesNum >= $iProfilesLimit) + return true; + + return false; + } + + /** + * Get number of profiles associated with the account + */ + public function getProfilesNumber ($isFilterNonSwitchableProfiles = true) + { + $a = $this->getProfilesIds($isFilterNonSwitchableProfiles); + return count($a); + } + + /** + * Get all profile ids associated with the account + */ + public function getProfilesIds ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true) + { + $a = $this->getProfiles ($isFilterNonSwitchableProfiles, $isFilterSystemProfiles); + return $a ? array_keys($a) : array(); + } + + /** + * Get all profiles associated with the account + */ + public function getProfiles ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true) + { + $oProfileQuery = BxDolProfileQuery::getInstance(); + $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID); + + if ($isFilterNonSwitchableProfiles) { + foreach ($aProfiles as $iProfileId => $aProfile) { + if ('system' == $aProfile['type']) + continue; + if (!BxDolService::call($aProfile['type'], 'act_as_profile')) + unset($aProfiles[$iProfileId]); + } + } + + if ($isFilterSystemProfiles) { + foreach ($aProfiles as $iProfileId => $aProfile) { + if ('system' == $aProfile['type']) + unset($aProfiles[$iProfileId]); + } + } + + return $aProfiles; + } + + /** + * Delete profile. + * @param $bDeleteWithContent - delete associated profiles with all its contents + */ + function delete($bDeleteWithContent = false) + { + $aAccountInfo = $this->_oQuery->getInfoById($this->_iAccountID); + if (!$aAccountInfo) + return false; + + // create system event before deletion + $isStopDeletion = false; + /** + * @hooks + * @hookdef hook-account-before_delete 'account', 'before_delete' - hook on before delete account, + * - $unit_name - equals `account` + * - $action - equals `before_delete` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `delete_with_content` - [bool] if account will delete with content = true, otherwise = false + * - `stop_deletion` - [bool] by ref, if it set to true account deletion will stopped, can be overridden in hook processing + * @hook @ref hook-account-before_delete + */ + bx_alert('account', 'before_delete', $this->_iAccountID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion)); + if ($isStopDeletion) + return false; + + $oAccountQuery = BxDolAccountQuery::getInstance(); + + $oProfileQuery = BxDolProfileQuery::getInstance(); + $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID); + foreach ($aProfiles as $iProfileId => $aRow) { + $oProfile = BxDolProfile::getInstance($iProfileId); + if (!$oProfile) + continue; + $oProfile->delete(false, $bDeleteWithContent, true); + } + + // delete profile + if (!$oAccountQuery->delete($this->_iAccountID)) + return false; + + // unset class instance to prevent creating the instance again + $sClass = __CLASS__ . '_' . $this->_iAccountID; + unset($GLOBALS['bxDolClasses'][$sClass]); + + /** + * @hooks + * @hookdef hook-account-delete 'account', 'delete' - hook on after delete account + * - $unit_name - equals `account` + * - $action - equals `delete` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `delete_with_content` - [bool] if account will delete with content = true, otherwise = false + * @hook @ref hook-account-delete + */ + bx_alert('account', 'delete', $this->_iAccountID, 0, array ('delete_with_content' => $bDeleteWithContent)); + + $this->doAudit($this->_iAccountID, $bDeleteWithContent ? '_sys_audit_action_account_deleted_with_content' : '_sys_audit_action_account_deleted'); + + return true; + } + + /** + * Add permament messages. + */ + public function addInformerPermanentMessages ($oInformer) + { + if (!$this->isConfirmed()) { + if (!$this->isConfirmedEmail()) { + $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email'); + $aAccountInfo = $this->getInfo(); + $oInformer->add('sys-account-unconfirmed-email', _t('_sys_txt_account_unconfirmed_email', $sUrl . '&resend=1', $aAccountInfo['email'], $sUrl), BX_INFORMER_ALERT); + } + if (!$this->isConfirmedPhone()) { + $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-phone') . ''; + $aAccountInfo = $this->getInfo(); + $oInformer->add('sys-account-unconfirmed-phone', _t('_sys_txt_account_unconfirmed_phone', $sUrl), BX_INFORMER_ALERT); + } + } + + $this->isNeedChangePassword(false, $oInformer); + } + + /** + * Get unsubscribe link for the specified mesage type + */ + public function getUnsubscribeLink($iEmailType, $iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $sUrl = ''; + switch ($iEmailType) { + case BX_EMAIL_NOTIFY: + $sUrl = 'page.php?i=unsubscribe-notifications'; + break; + case BX_EMAIL_MASS: + $sUrl = 'page.php?i=unsubscribe-news'; + break; + default: + return ''; + } + return BxDolPermalinks::getInstance()->permalink($sUrl) . '&id=' . $iAccountId . '&code=' . $this->getEmailHash(); + } + + public function getEmailHash($iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + $a = $this->getInfo(); + return md5($a['email'] . $a['salt'] . BX_DOL_SECRET); + } + + public function getPasswordExpiredDate($iPasswordExpiredForMembership, $iAccountId = false) + { + if ($iPasswordExpiredForMembership == 0) + return 0; + + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $aAccountInfo = $this->_oQuery->getInfoById($iAccountId); + + $iLastPassChanged = $this->_oQuery->getLastPasswordChanged($iAccountId); + if ($iLastPassChanged == 0) + $iLastPassChanged = $aAccountInfo['added']; + + return $iPasswordExpiredForMembership * 86400 + $iLastPassChanged; + } + + public function getPasswordExpiredDateByAccount($iAccountId = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $oACL = BxDolAcl::getInstance(); + + $aProfiles = BxDolAccount::getInstance($iAccountId)->getProfiles(); + $iPasswordExpiredForMembership = 0; + foreach ($aProfiles as $aProfile) { + $aMembersipInfo = $oACL->getMemberMembershipInfo($aProfile['id']); + $Memberships = []; + BxDolAclQuery::getInstance()->getLevels(['type' => 'by_id', 'value' => $aMembersipInfo['id']], $aMembership); + if($aMembership['password_expired'] > 0){ + if ($iPasswordExpiredForMembership > 0 && $aMembership['password_expired'] < $iExpired) + $iPasswordExpiredForMembership = $aMembership['password_expired']; + if ($iPasswordExpiredForMembership == 0 ) + $iPasswordExpiredForMembership = $aMembership['password_expired']; + } + } + + return $this->getPasswordExpiredDate($iPasswordExpiredForMembership, $iAccountId); + } + + public function isNeedChangePassword($iAccountId = false, $oInformer = false) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + + $aAccountInfo = $this->getInfo(); + list($sPageLink, $aPageParams) = bx_get_base_url_inline(); + $bNeedRedirectToChangePassword = true; + + if (isset($aPageParams['i']) && $aPageParams['i'] == 'account-settings-password') + $bNeedRedirectToChangePassword = false; + + /** + * @hooks + * @hookdef hook-account-is_need_to_change_password 'account', 'is_need_to_change_password' - hook on after delete account + * - $unit_name - equals `account` + * - $action - equals `is_need_to_change_password` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [bool] by ref, if Need Redirect To Change Password = true, otherwise = false, can be overridden in hook processing + * @hook @ref hook-account-is_need_to_change_password + */ + bx_alert('account', 'is_need_to_change_password', $iAccountId, false, ['override_result' => &$bNeedRedirectToChangePassword]); + + if ($aAccountInfo['password_expired'] > 0 && $aAccountInfo['password_expired'] < time() && $bNeedRedirectToChangePassword) { + if (getParam('sys_account_accounts_force_password_change_after_expiration') == 'on'){ + header('Location: ' . BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-password')); + exit; + } + else { + if(!$oInformer) + $oInformer = BxDolInformer::getInstance(); + + $oInformer->add('sys-account-need-to-change-password', _t('_sys_txt_account_need_to_change_password', BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-password')), BX_INFORMER_ALERT); + } + } + } + + public function doAudit($iAccountId, $sAction, $aData = array()) + { + $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID; + bx_audit( + $iAccountId, + 'bx_accounts', + $sAction, + array('content_title' => $this->getEmail(), 'data' => $aData) + ); + } + + /** + * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. + */ + static public function isAllowedCreate ($iProfileId, $isPerformAction = false) + { + $aCheck = checkActionModule($iProfileId, 'create account', 'system', $isPerformAction); + if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED) + return MsgBox($aCheck[CHECK_ACTION_MESSAGE]); + return CHECK_ACTION_RESULT_ALLOWED; + } + + static public function isAllowedCreateMultiple ($iProfileId, $isPerformAction = false) + { + $iLimit = (int)getParam('sys_account_limit_profiles_number'); + + $bResult = false; + if(isAdmin() || $iLimit != 1) + $bResult = true; + + $oProfile = BxDolProfile::getInstance($iProfileId); + if($iLimit == 1 && $oProfile->getModule() == 'system') + $bResult = true; + + if($oProfile && $oProfile->getAccountId() != getLoggedId()) + $bResult = false; + + /** + * @hooks + * @hookdef hook-account-allow_create_another_profile 'profile', 'allow_create_another_profile' - hook on check allow create profile + * - $unit_name - equals `profile` + * - $action - equals `allow_create_another_profile` + * - $object_id - profile id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [bool] by ref, if allow create another profile = true, otherwise = false, can be overridden in hook processing + * @hook @ref hook-account-allow_create_another_profile + */ + bx_alert('profile', 'allow_create_another_profile', $iProfileId, 0, [ + 'override_result' => &$bResult + ]); + + return $bResult; + } + + /** + * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. + */ + static public function isAllowedEdit ($iProfileId, $aContentInfo, $isPerformAction = false) + { + $oProfile = BxDolProfile::getInstance($iProfileId); + if (!$oProfile) + return _t('_sys_txt_access_denied'); + + $aProfileInfo = $oProfile->getInfo(); + if (!$aProfileInfo || getLoggedId() != $aProfileInfo['account_id']) + return _t('_sys_txt_access_denied'); + + return CHECK_ACTION_RESULT_ALLOWED; + } + + /** + * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. + */ + static public function isAllowedDelete ($iProfileId, $aContentInfo, $isPerformAction = false) + { + $iAccountId = (int)BxDolProfile::getInstance($iProfileId)->getAccountId(); + if(isAdmin($iAccountId) && $iAccountId == (int)$aContentInfo['id']) + return _t('_sys_txt_account_cannot_delete'); + + $aCheck = checkActionModule($iProfileId, 'delete account', 'system', $isPerformAction); + if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED) + return $aCheck[CHECK_ACTION_MESSAGE]; + + return CHECK_ACTION_RESULT_ALLOWED; + } + + static public function pruning () + { + $iCount = 0; + $iTime = time() - getParam('sys_account_accounts_pruning_interval') * 86400; + $oAccountQuery = BxDolAccountQuery::getInstance(); + $aAccounts = []; + $aAccountsSuspend = []; + $bSuspend = false; + $aPruning = explode(',', getParam('sys_account_accounts_pruning')); + foreach ($aPruning as $sPruning) { + switch($sPruning) { + case 'no_login_suspend': + $aAccountsSuspend = $oAccountQuery->getAccountsForPruning('no_login', $iTime); + break; + + case 'no_login_delete': + $aAccounts = array_merge($aAccounts, $oAccountQuery->getAccountsForPruning('no_login', $iTime)); + break; + + + + case 'no_confirm_delete': + $aAccounts1 = $oAccountQuery->getAccountsForPruning('no_confirm', $iTime); + foreach ($aAccounts1 as $k => $aAccount) { + $oAccount = BxDolAccount::getInstance($aAccount['id']); + if($oAccount->isConfirmed()){ + unset($aAccounts1[$k]); + } + } + $aAccounts = array_merge($aAccounts, $aAccounts1); + break; + + case 'no_profile_delete': + $aAccounts = array_merge($aAccounts, $aAccounts = $oAccountQuery->getAccountsForPruning('no_profile', $iTime)); + break; + } + } + + foreach ($aAccountsSuspend as $k => $aAccount) { + $oAccount = BxDolAccount::getInstance($aAccount['id']); + $oProfile = BxDolProfile::getInstanceAccountProfile($aAccount['id']); + $oProfile->suspend(BX_PROFILE_ACTION_AUTO, 0 ,false); + $aProfiles = $oAccount->getProfiles(); + foreach($aProfiles as $aProfile){ + BxDolProfile::getInstance($aProfile['id'])->suspend(BX_PROFILE_ACTION_AUTO, 0 ,false); + } + $iCount++; + } + + + foreach ($aAccounts as $k => $aAccount) { + $oAccount = BxDolAccount::getInstance($aAccount['id']); + $oAccount->delete(false); + $iCount++; + } + + return $iCount; + } + +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAclQuery.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAclQuery.php new file mode 100644 index 0000000000..f96ae7b93d --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolAclQuery.php @@ -0,0 +1,495 @@ + 'getAll', 'params' => array(0 => 'query')); + $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = ""; + + if(!isset($aParams['order']) || empty($aParams['order'])) + $sOrderClause = "ORDER BY `tal`.`Order` ASC"; + + switch($aParams['type']) { + case 'by_id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1] = array( + 'id' => $aParams['value'] + ); + + $sWhereClause .= "AND `tal`.`ID`=:id"; + $sLimitClause .= "LIMIT 1"; + break; + + case 'all_active': + $sWhereClause .= "AND `tal`.`Active`='yes'"; + break; + + case 'all_active_purchasble_pair': + $aMethod['name'] = "getPairs"; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + $sWhereClause .= "AND `tal`.`Active`='yes' AND `tal`.`Purchasable`='yes'"; + break; + + case 'all_active_pair': + $aMethod['name'] = "getPairs"; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + $sWhereClause .= "AND `tal`.`Active`='yes'"; + break; + + case 'all_active_not_automatic_pair': + $aMethod['name'] = "getPairs"; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + $sWhereClause .= "AND `tal`.`Active`='yes' AND `tal`.`ID` NOT IN (" . $this->implode_escape(array( + MEMBERSHIP_ID_NON_MEMBER, + MEMBERSHIP_ID_ACCOUNT, + MEMBERSHIP_ID_UNCONFIRMED, + MEMBERSHIP_ID_PENDING, + MEMBERSHIP_ID_SUSPENDED + + )) . ")"; + break; + + case 'all_pair': + $aMethod['name'] = "getPairs"; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = 'name'; + break; + + case 'all_order_id': + $sOrderClause = "ORDER BY `tal`.`ID` ASC"; + break; + + case 'password_can_expired': + $sWhereClause .= "AND `tal`.`PasswordExpired` <> 0"; + break; + + case 'all': + break; + } + + $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . " + `tal`.`ID` AS `id`, + `tal`.`Name` AS `name`, + `tal`.`Icon` AS `icon`, + `tal`.`Description` AS `description`, + `tal`.`Active` AS `active`, + `tal`.`Purchasable` AS `purchasable`, + `tal`.`Removable` AS `removable`, + `tal`.`QuotaSize` AS `quota_size`, + `tal`.`QuotaNumber` AS `quota_number`, + `tal`.`QuotaMaxFileSize` AS `quota_max_file_size`, + `tal`.`PasswordExpired` AS `password_expired`, + `tal`.`PasswordExpiredNotify` AS `password_expired_notify`, + `tal`.`Order` AS `order`" . $sSelectClause . " + FROM `sys_acl_levels` AS `tal` " . $sJoinClause . " + WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause; + $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']); + + if(!$bReturnCount) + return !empty($aItems); + + return (int)$this->getOne("SELECT FOUND_ROWS()"); + } + + function getActions($aParams, &$aItems, $bReturnCount = true) + { + $sMemoryKey = ''; + $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query')); + $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = ""; + + if(!isset($aParams['order']) || empty($aParams['order'])) + $sOrderClause = "ORDER BY `taa`.`Title` ASC"; + + + switch($aParams['type']) { + case 'by_names_and_module': + $aMethod['params'][1] = array( + 'module' => $aParams['module'] + ); + + $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ") AND `taa`.`Module` = :module "; + $sMemoryKey = 'BxDolAclQuery::getActions' . $aParams['type'] . $aParams['module'] . $sWhereClause; + break; + + case 'by_names': + $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ")"; + break; + + case 'by_level_id': + $aMethod['params'][1] = array( + 'level_id' => $aParams['value'], + 'level_code' => pow(2, ($aParams['value'] - 1)) + ); + + $sSelectClause .= ", `tam`.`AllowedCount` AS `allowed_count`, `tam`.`AllowedPeriodLen` AS `allowed_period_len`, `tam`.`AllowedPeriodStart` AS `allowed_period_start`, `tam`.`AllowedPeriodEnd` AS `allowed_period_end`, `tam`.`AdditionalParamValue` AS `additional_param_value` "; + $sJoinClause .= "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` "; + $sWhereClause .= "AND `tam`.`IDLevel`=:level_id AND (`taa`.`DisabledForLevels`='0' OR `taa`.`DisabledForLevels`&:level_code=0)"; + break; + + case 'by_level_id_key_id': + $aMethod['name'] = 'getAllWithKey'; + $aMethod['params'][1] = 'id'; + $aMethod['params'][2] = array( + 'level_id' => $aParams['value'] + ); + + $sSelectClause .= ", `tam`.`AllowedCount` AS `allowed_count`, `tam`.`AllowedPeriodLen` AS `allowed_period_len` "; + $sJoinClause .= "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` "; + $sWhereClause .= "AND `tam`.`IDLevel`=:level_id"; + break; + + case 'counter_by_modules': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'module'; + $aMethod['params'][2] = 'counter'; + $sSelectClause = ", COUNT(*) AS `counter`"; + $sGroupClause = "GROUP BY `taa`.`Module`"; + break; + + case 'counter_by_levels': + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'level_id'; + $aMethod['params'][2] = 'counter'; + $sSelectClause = ", `tam`.`IDLevel` AS `level_id`, COUNT(`tam`.`IDAction`) AS `counter`"; + $sJoinClause = "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` AND (`taa`.`DisabledForLevels`='0' OR `taa`.`DisabledForLevels`&POW(2, `tam`.`IDLevel`-1)=0) "; + $sGroupClause = "GROUP BY `tam`.`IDLevel`"; + break; + } + + $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . " + `taa`.`ID` AS `id`, + `taa`.`Module` AS `module`, + `taa`.`Name` AS `name`, + `taa`.`Title` AS `title`, + `taa`.`Countable` AS `countable`, + `taa`.`DisabledForLevels` AS `disabled_for_levels`" . $sSelectClause . " + FROM `sys_acl_actions` AS `taa` " . $sJoinClause . " + WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause; + + if ($sMemoryKey) { + array_unshift($aMethod['params'], $sMemoryKey, $aMethod['name']); + $aItems = call_user_func_array(array($this, 'fromMemory'), $aMethod['params']); + return $bReturnCount ? count($aItems) : !empty($aItems); + } + + $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']); + + if(!$bReturnCount) + return !empty($aItems); + + return (int)$this->getOne("SELECT FOUND_ROWS()"); + } + + /** + * Fetch the last purchased/assigned membership that is still active for the given profile. + * + * NOTE. Don't use cache here, because it's causing an error, if a number of memberrship levels are purchased at the same time. + * fromMemory returns the same DateExpires because setMembership (old buyMembership) function is called in cycle in the same session. + */ + function getLevelCurrent($iProfileId, $iTime = 0) + { + $iTime = $iTime == 0 ? time() : (int)$iTime; + + $sSql = $this->prepare(" + SELECT `sys_acl_levels_members`.`IDLevel` as `id`, + `sys_acl_levels`.`Name` AS `name`, + `sys_acl_levels`.`QuotaSize` AS `quota_size`, + `sys_acl_levels`.`QuotaNumber` AS `quota_number`, + `sys_acl_levels`.`QuotaMaxFileSize` AS `quota_max_file_size`, + UNIX_TIMESTAMP(`sys_acl_levels_members`.`DateStarts`) as `date_starts`, + UNIX_TIMESTAMP(`sys_acl_levels_members`.`DateExpires`) as `date_expires`, + `sys_acl_levels_members`.`State` AS `state`, + `sys_acl_levels_members`.`TransactionID` AS `transaction_id`, + `sys_profiles`.`status` + FROM `sys_acl_levels_members` + RIGHT JOIN `sys_profiles` ON `sys_acl_levels_members`.IDMember = `sys_profiles`.`id` + AND (`sys_acl_levels_members`.DateStarts IS NULL OR `sys_acl_levels_members`.DateStarts <= FROM_UNIXTIME(?)) + AND (`sys_acl_levels_members`.DateExpires IS NULL OR `sys_acl_levels_members`.DateExpires > FROM_UNIXTIME(?)) + LEFT JOIN `sys_acl_levels` ON `sys_acl_levels_members`.IDLevel = `sys_acl_levels`.ID + WHERE `sys_profiles`.`id` = ? + ORDER BY `sys_acl_levels_members`.DateStarts DESC + LIMIT 1", $iTime, $iTime, $iProfileId); + + return $this->fromMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . $iTime, 'getRow', $sSql); + } + + function getLevelByIdCached($iLevel) + { + $sQuery = $this->prepare("SELECT + `tal`.`ID` AS `id`, + `tal`.`Name` AS `name`, + `tal`.`QuotaSize` AS `quota_size`, + `tal`.`QuotaNumber` AS `quota_number`, + `tal`.`QuotaMaxFileSize` AS `quota_max_file_size` + FROM `sys_acl_levels` AS `tal` + WHERE `tal`.`ID`=? + LIMIT 1", $iLevel); + return $this->fromCache('sys_acl_levels' . $iLevel, 'getRow', $sQuery); + } + + function getAction($iMembershipId, $iActionId) + { + $sQuery = $this->prepare("SELECT + `tam`.`IDAction` AS `id`, + `taa`.`Name` AS `name`, + `taa`.`Title` AS `title`, + `tam`.`AllowedCount` AS `allowed_count`, + `tam`.`AllowedPeriodLen` AS `allowed_period_len`, + UNIX_TIMESTAMP(`tam`.`AllowedPeriodStart`) as `allowed_period_start`, + UNIX_TIMESTAMP(`tam`.`AllowedPeriodEnd`) as `allowed_period_end`, + `tam`.`AdditionalParamValue` AS `additional_param_value` + FROM `sys_acl_actions` AS `taa` + LEFT JOIN `sys_acl_matrix` AS `tam` ON `tam`.`IDAction` = `taa`.`ID` AND `tam`.`IDLevel` = ? + WHERE `taa`.`ID` = ?", $iMembershipId, $iActionId); + return $this->fromMemory('BxDolAclQuery::getAction' . $iMembershipId . $iActionId, 'getRow', $sQuery); + } + + function getActionTrack($iActionId, $iProfileId) + { + $sQuery = $this->prepare("SELECT + `taat`.`ActionsLeft` AS `actions_left`, + UNIX_TIMESTAMP(`taat`.`ValidSince`) as `valid_since` + FROM `sys_acl_actions_track` AS `taat` + WHERE `taat`.`IDAction`=? AND `taat`.`IDMember`=?", $iActionId, $iProfileId); + return $this->getRow($sQuery); + } + + function insertActionTarck($iActionId, $iProfileId, $iActionsLeft, $iValidSince) + { + $sQuery = $this->prepare("INSERT INTO `sys_acl_actions_track`(`IDAction`, `IDMember`, `ActionsLeft`, `ValidSince`) VALUES (?, ?, ?, FROM_UNIXTIME(?))", $iActionId, $iProfileId, $iActionsLeft, $iValidSince); + return (int)$this->query($sQuery) > 0; + } + + function updateActionTrack($iActionId, $iProfileId, $iActionsLeft, $iValidSince = 0) + { + $aBindings = array( + 'actions_left' => $iActionsLeft, + 'action_id' => $iActionId, + 'member_id' => $iProfileId + ); + + $sUpdateAddon = ""; + if($iValidSince != 0) { + $aBindings['valid_since'] = $iValidSince; + + $sUpdateAddon = ", ValidSince=FROM_UNIXTIME(:valid_since)"; + } + + $sQuery = "UPDATE `sys_acl_actions_track` SET `ActionsLeft`=:actions_left" . $sUpdateAddon . " WHERE `IDAction`=:action_id AND `IDMember`=:member_id"; + return (int)$this->query($sQuery, $aBindings) > 0; + } + + function insertLevelByProfileId($iProfileId, $iMembershipId, $iDateStarts, $aPeriod, $sTransactionId) + { + $aBindings = array( + 'member_id' => $iProfileId, + 'level_id' => $iMembershipId, + 'transaction_id' => $sTransactionId, + 'date_starts' => $iDateStarts + ); + + $sSetClause = ''; + if((int)$aPeriod['period'] != 0) { + $aBindings['period'] = (int)$aPeriod['period']; + + switch($aPeriod['period_unit']) { + case MEMBERSHIP_PERIOD_UNIT_DAY: + case MEMBERSHIP_PERIOD_UNIT_WEEK: + if($aPeriod['period_unit'] == MEMBERSHIP_PERIOD_UNIT_WEEK) + $aBindings['period'] *= 7; + + $sSetClause = "DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period DAY)"; + break; + + case MEMBERSHIP_PERIOD_UNIT_MONTH: + $sSetClause = "DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period MONTH)"; + break; + + case MEMBERSHIP_PERIOD_UNIT_YEAR: + $sSetClause = "DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period YEAR)"; + break; + } + + if(!empty($sSetClause) && !empty($aPeriod['period_reserve'])) { + $aBindings['reserve'] = (int)$aPeriod['period_reserve']; + + $sSetClause = "DATE_ADD(" . $sSetClause . ", INTERVAL :reserve DAY)"; + } + + if(!empty($sSetClause)) + $sSetClause = ", `DateExpires`=" . $sSetClause; + + if(isset($aPeriod['period_trial']) && $aPeriod['period_trial'] === true) { + $aBindings['state'] = 'trial'; + + $sSetClause .= ", `State`=:state"; + } + } + + $sQuery = $this->prepare("INSERT `sys_acl_levels_members` SET `IDMember`=:member_id, `IDLevel`=:level_id, `DateStarts`=FROM_UNIXTIME(:date_starts), `TransactionID`=:transaction_id" . $sSetClause); + return (int)$this->query($sQuery, $aBindings) > 0; + } + + function deleteLevelByProfileId($iProfileId, $bAll = false) + { + $sQuery = $this->prepare("DELETE FROM `sys_acl_levels_members` WHERE `IDMember` = ? " . ($bAll ? "" : " AND (`DateExpires` IS NULL OR `DateExpires` > NOW())"), $iProfileId); + return (int)$this->query($sQuery) > 0; + } + + function deleteLevelBy($aWhere) + { + if(empty($aWhere)) + return false; + + $sQuery = "DELETE FROM `sys_acl_levels_members` WHERE " . $this->arrayToSQL($aWhere, ' AND '); + return (int)$this->query($sQuery) > 0; + } + + function maintenance($iDaysToCleanMemLevels = 0) + { + $sQuery = $this->prepare("DELETE FROM `sys_acl_levels_members` WHERE `DateExpires` < NOW() - INTERVAL ? DAY", $iDaysToCleanMemLevels); + if ($iDeleteMemLevels = $this->query($sQuery)) + $this->query("OPTIMIZE TABLE `sys_acl_levels_members`"); + return $iDeleteMemLevels; + } + + function clearActionsTracksForMember($iMemberId) + { + $sQuery = $this->prepare("DELETE FROM `sys_acl_actions_track` WHERE `IDMember` = ?", (int)$iMemberId); + return $this->query($sQuery); + } + + function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId) + { + $sJoin = $sWhere = ""; + $iLevelId = !is_array($mixedLevelId) ? $mixedLevelId : 0; + if (!$iLevelId && is_array($mixedLevelId) && 1 == count($mixedLevelId)) { + $a = array_values($mixedLevelId); + $iLevelId = array_shift($a); + } + + // unconfirmed + if (MEMBERSHIP_ID_UNCONFIRMED == $iLevelId) { + return array( + 'where' => " AND `cblasp_a`.`email_confirmed` = 0 ", + 'join' => " INNER JOIN `sys_profiles` AS `cblasp_p` ON (`" . $sContentTable . "`.`" . $sContentField . "`=`cblasp_p`.`id`) INNER JOIN `sys_accounts` AS `cblasp_a` ON (`cblasp_a`.`id`=`cblasp_p`.`account_id`) ", + ); + } + // standard + elseif (MEMBERSHIP_ID_STANDARD == $iLevelId) { + + $sWhere .= " AND (`tlm`.`DateStarts` IS NULL OR `tlm`.`DateStarts` <= NOW()) AND (`tlm`.`DateExpires` IS NULL OR `tlm`.`DateExpires` > NOW()) AND `tlm`.`IDMember` IS NULL AND `cblasp_a`.`email_confirmed` != 0 "; + + $sJoin .= " LEFT JOIN `sys_acl_levels_members` AS `tlm` ON `" . $sContentTable . "`.`" . $sContentField . "`=`tlm`.`IDMember` INNER JOIN `sys_profiles` AS `cblasp_p` ON (`" . $sContentTable . "`.`" . $sContentField . "`=`cblasp_p`.`id`) INNER JOIN `sys_accounts` AS `cblasp_a` ON (`cblasp_a`.`id`=`cblasp_p`.`account_id`) "; + + return array( + 'where' => $sWhere, + 'join' => $sJoin + ); + } + // other levels + else { + + if(is_array($mixedLevelId)) + $sWhere .= " AND `tlm`.`IDLevel` IN (" . $this->implode_escape($mixedLevelId) . ")"; + else + $sWhere .= $this->prepareAsString(" AND `tlm`.`IDLevel` = ?", (int)$mixedLevelId); + + $sWhere .= " AND (`tlm`.`DateStarts` IS NULL OR `tlm`.`DateStarts` <= NOW()) AND (`tlm`.`DateExpires` IS NULL OR `tlm`.`DateExpires` > NOW()) AND `cblasp_a`.`email_confirmed` != 0 "; + + $sJoin .= " INNER JOIN `sys_acl_levels_members` AS `tlm` ON `" . $sContentTable . "`.`" . $sContentField . "`=`tlm`.`IDMember` INNER JOIN `sys_profiles` AS `cblasp_p` ON (`" . $sContentTable . "`.`" . $sContentField . "`=`cblasp_p`.`id`) INNER JOIN `sys_accounts` AS `cblasp_a` ON (`cblasp_a`.`id`=`cblasp_p`.`account_id`) "; + + return array( + 'where' => $sWhere, + 'join' => $sJoin + ); + } + } + + function getContentByActionAsSQLPart($sContentTable, $sContentField, $mixedActionName, $aParams = []) + { + $sWhere = " AND (`tlm`.`DateStarts` IS NULL OR `tlm`.`DateStarts` <= NOW()) AND (`tlm`.`DateExpires` IS NULL OR `tlm`.`DateExpires` > NOW()) "; + + $sJoinWhere = ""; + if(is_array($mixedActionName)) + $sJoinWhere .= " AND `ta`.`Name` IN (" . $this->implode_escape($mixedActionName) . ")"; + else + $sJoinWhere .= $this->prepareAsString(" AND `ta`.`Name` = ?", $mixedActionName); + + if(!empty($aParams['module'])) + $sJoinWhere .= $this->prepareAsString(" AND `ta`.`Module` = ?", $aParams['module']); + + $sJoin = " INNER JOIN `sys_acl_levels_members` AS `tlm` ON `" . $sContentTable . "`.`" . $sContentField . "`=`tlm`.`IDMember` INNER JOIN `sys_acl_matrix` AS `tm` ON `tlm`.`IDLevel`=`tm`.`IDLevel` INNER JOIN `sys_acl_actions` AS `ta` ON (`tm`.`IDAction`=`ta`.`ID` " . $sJoinWhere . ") "; + + return array( + 'where' => $sWhere, + 'join' => $sJoin + ); + } + + function getProfilesByMembership($mixedLevelId) + { + $aSqlParts = $this->getContentByLevelAsSQLPart('sys_profiles', 'id', $mixedLevelId); + + return $this->getAll("SELECT `sys_profiles`.* FROM `sys_profiles`" . $aSqlParts['join'] . " WHERE 1" . $aSqlParts['where']); + } + + function getProfilesByAction($mixedActionName, $aParams = []) + { + $sMethod = "getAll"; + $sSqlSelect = "`sys_profiles`.*"; + if(isset($aParams['ids_only']) && $aParams['ids_only'] === true) { + $sMethod = "getColumn"; + $sSqlSelect = "`sys_profiles`.`id`"; + } + + $aSqlParts = $this->getContentByActionAsSQLPart('sys_profiles', 'id', $mixedActionName, $aParams); + + return $this->$sMethod("SELECT DISTINCT " . $sSqlSelect . " FROM `sys_profiles`" . $aSqlParts['join'] . " WHERE 1" . $aSqlParts['where']); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmts.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmts.php new file mode 100644 index 0000000000..538ec1afa8 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmts.php @@ -0,0 +1,2575 @@ + on saving, 1 - standard(default) visual editor, 2 - full visual editor, 3 - mini visual editor. + * - PerView - number of comments on a page + * - IsRatable - 0 or 1 allow to rate comments or not + * - ViewingThreshold - comment viewing treshost, if comment is below this number it is hidden by default + * - IsOn - is this comment object enabled + * - RootStylePrefix - toot comments style prefix, if you need root comments look different\ + * - ObjectVote - Vote object name to process comments' votes. May be empty if Comment Vote is not needed. + * - TriggerTable - table to be updated upon each comment + * - TriggerFieldId - TriggerTable table field with unique record id of + * - TriggerFieldComments - TriggerTable table field with comments count, it will be updated automatically upon eaech comment + * - ClassName - your custom class name if you need to override default class, this class must have the same constructor arguments + * - ClassFile - file where your ClassName is stored. + * + * You can refer to BoonEx modules for sample record in this table. + * + * + * + * @section example Example of usage: + * After filling in the table you can show comments section in any place, using the following code: + * + * @code + * $o = new BxTemplCmts('value of ObjectName field', $iYourEntryId); + * if ($o->isEnabled()) + * echo $o->getCommentsBlock (); + * @endcode + * + * Please note that you never need to use BxDolCmts class directly, use BxTemplCmts instead. + * Also if you override comments class with your own then make it child of BxTemplCmts class. + * + * + * + * @section acl Memberships/ACL: + * - comments post + * - comments edit own + * - comments remove own + * - comments edit all + * + * + * + * @section alerts Alerts: + * Alerts type/unit - every module has own type/unit, it equals to ObjectName. + * + * The following alerts are rised + * + * - commentPost - comment was posted + * - $iObjectId - entry id + * - $iSenderId - author of comment + * - $aExtra['comment_id'] - just added comment id + * + * - commentRemoved - comments was removed + * - $iObjectId - entry id + * - $iSenderId - comment deleter id + * - $aExtra['comment_id'] - removed comment id + * + * - commentUpdated - comments was updated + * - $iObjectId - entry id + * - $iSenderId - comment deleter id + * - $aExtra['comment_id'] - updated comment id + * + * - commentRated - comments was rated + * - $iObjectId - entry id + * - $iSenderId - comment rater id + * - $aExtra['comment_id'] - rated comment id + * - $aExtra['rate'] - comment rate 1 or -1 + * + */ +class BxDolCmts extends BxDolFactory implements iBxDolReplaceable, iBxDolContentInfoService +{ + /** + * System tables which are UNITED for all + * comment systems and cannot be overwritten. + */ + public static $sTableSystems = 'sys_objects_cmts'; + public static $sTableIds = 'sys_cmts_ids'; + + /** + * System tables which are used by default and + * can be overwritten in comment systems. + * @see BxDolCmts::getTableNameImages and BxDolCmts::getTableNameImages2Entries + */ + protected $_sTableImages = 'sys_cmts_images'; + protected $_sTableImages2Entries = 'sys_cmts_images2entries'; + + protected $_aElementDefaults; + protected $_aElementDefaultsApi; + protected $_aElementParamsApi; //--- Params from DefaultsApi array to be passed to Api + + protected $_bIsApi; + + protected $_sType; + protected $_oQuery = null; + protected $_oTemplate = null; + + protected $_bPostQuote; + protected $_bMinPostForm; + protected $_sFormObject; + protected $_sFormDisplayPost; + protected $_sFormDisplayEdit; + + protected $_sConnObjFriends; + protected $_sConnObjSubscriptions; + + protected $_sMenuObjManage; + protected $_sMenuObjActions; + protected $_sMenuObjCounters; + protected $_sMenuObjMeta; + + protected $_sMetatagsObj; + + protected $_sViewUrl = ''; + protected $_sBaseUrl = ''; + protected $_sListAnchor = ''; + protected $_sItemAnchor = ''; + + protected $_aSystems = []; + + protected $_sSystem = 'profile'; ///< current comment system name + protected $_aSystem = []; ///< current comments system array + protected $_iId = 0; ///< obect id to be commented + protected $_iAuthorId = 0; ///< currently logged in user who browse, post, etc. + + protected $_aParams = []; + protected $_aT = []; ///< an array of lang keys + protected $_aMarkers = []; + + protected $_sDisplayType = ''; + protected $_sDpSessionKey = ''; + protected $_iDpMaxLevel = 0; + + protected $_sBrowseType = ''; + protected $_bBrowseFilter = false; + protected $_sBrowseFilter = ''; + protected $_sBpSessionKeyType = ''; + protected $_sBpSessionKeyFilter = ''; + protected $_aOrder = array(); + + protected $_sSnippetLenthLiveSearch = 50; + + protected $_iRememberTime = 2592000; + + protected $_bLiveUpdates = true; + + /** + * Constructor + * $sSystem - comments system name + * $iId - obect id to be commented + */ + protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false) + { + parent::__construct(); + + $this->_bIsApi = bx_is_api(); + + $this->_sType = BX_DOL_CMT_TYPE_COMMENTS; + + $this->_aSystems = $this->getSystems(); + if(!isset($this->_aSystems[$sSystem])) + return; + + $this->_sSystem = $sSystem; + $this->_aSystem = $this->_aSystems[$sSystem]; + $this->_aSystem['is_browse_filter'] = (int)$this->_bBrowseFilter; + + $this->_iAuthorId = $this->_getAuthorId(); + + $this->_iDpMaxLevel = (int)$this->_aSystem['number_of_levels']; + $this->_sDisplayType = $this->_iDpMaxLevel == 0 ? BX_CMT_DISPLAY_FLAT : BX_CMT_DISPLAY_THREADED; + $this->_sDpSessionKey = 'bx_' . $this->_sSystem . '_dp_'; + + $this->_sBrowseType = $this->_aSystem['browse_type']; + $this->_sBrowseFilter = BX_CMT_FILTER_ALL; + $this->_sBpSessionKeyType = 'bx_' . $this->_sSystem . '_bpt_'; + $this->_sBpSessionKeyFilter = 'bx_' . $this->_sSystem . '_bpf_'; + $this->_aOrder = array( + 'by' => BX_CMT_ORDER_BY_DATE, + 'way' => BX_CMT_ORDER_WAY_ASC + ); + + list($mixedUserDp, $mixedUserBpType, $mixedUserBpFilter) = $this->_getUserChoice(); + if(!empty($mixedUserDp)) + $this->_sDisplayType = $mixedUserDp; + if(!empty($mixedUserBpType)) + $this->_sBrowseType = $mixedUserBpType; + if(!empty($mixedUserBpFilter)) + $this->_sBrowseFilter = $mixedUserBpFilter; + + $this->_sViewUrl = 'page.php?i=cmts-view'; + $this->_sBaseUrl = $this->_aSystem['base_url']; + + $this->_sListAnchor = "cmts-anchor-%s-%d"; + $this->_sItemAnchor = "cmt-anchor-%s-%d-%d"; + + $this->_oQuery = new BxDolCmtsQuery($this); + + $this->_bPostQuote = false; + $this->_bMinPostForm = true; + $this->_sFormObject = 'sys_comment'; + $this->_sFormDisplayPost = 'sys_comment_post'; + $this->_sFormDisplayEdit = 'sys_comment_edit'; + + $this->_sConnObjFriends = 'sys_profiles_friends'; + $this->_sConnObjSubscriptions = 'sys_profiles_subscriptions'; + + $this->_sMenuObjManage = 'sys_cmts_item_manage'; + $this->_sMenuObjActions = 'sys_cmts_item_actions'; + $this->_sMenuObjCounters = 'sys_cmts_item_counters'; + $this->_sMenuObjMeta = 'sys_cmts_item_meta'; + + $this->_sMetatagsObj = 'sys_cmts'; + + if(($aParams = bx_get('params')) !== false) { + if(is_string($aParams)) + $aParams = json_decode($aParams, true); + if(!empty($aParams) && is_array($aParams)) + $this->_aParams = array_merge($this->_aParams, $aParams); + } + + $this->_aT = [ + 'block_comments_title' => '_cmt_block_comments_title', + 'txt_sample_single' => '_cmt_txt_sample_comment_single', + 'txt_sample_vote_single' => '_cmt_txt_sample_vote_single', + 'txt_sample_reaction_single' => '_cmt_txt_sample_reaction_single', + 'txt_sample_score_up_single' => '_cmt_txt_sample_score_up_single', + 'txt_sample_score_down_single' => '_cmt_txt_sample_score_down_single', + 'txt_min_form_placeholder' => '_cmt_txt_min_form_placeholder' + ]; + + if ($iInit) + $this->init($iId); + + if ($oTemplate) + $this->_oTemplate = $oTemplate; + else + $this->_oTemplate = BxDolTemplate::getInstance(); + } + + /** + * get comments object instanse + * @param $sSys comments object name + * @param $iId associated content id, where comments are postred in + * @param $iInit perform initialization + * @return null on error, or ready to use class instance + */ + public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false) + { + if(isset($GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId])) + return $GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId]; + + $aSystems = self::getSystems(); + if (!isset($aSystems[$sSys])) + return null; + + $sClassName = 'BxTemplCmts'; + if(!empty($aSystems[$sSys]['class_name'])) { + $sClassName = $aSystems[$sSys]['class_name']; + if(!empty($aSystems[$sSys]['class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']); + } + + $o = new $sClassName($sSys, $iId, $iInit, $oTemplate); + return ($GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId] = $o); + } + + /** + * get comments object instanse + * @param $iUniqId unique comment id + * @return null on error, or ready to use class instance + */ + public static function getObjectInstanceByUniqId($iUniqId, $iInit = true, $oTemplate = false) + { + $aData = BxDolCmtsQuery::getInfoByUniqId($iUniqId); + if(empty($aData) || !is_array($aData)) + return null; + + return self::getObjectInstance($aData['system_name'], $aData['cmt_object_id']); + } + + public static function &getSystems () + { + $sKey = 'bx_dol_cache_memory_cmts_systems'; + + if (!isset($GLOBALS[$sKey])) { + $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_cmts', 'getAllWithKey', ' + SELECT + `ID` as `system_id`, + `Name` AS `name`, + `Module` AS `module`, + `Table` AS `table`, + `CharsPostMin` AS `chars_post_min`, + `CharsPostMax` AS `chars_post_max`, + `CharsDisplayMax` AS `chars_display_max`, + `Html` AS `html`, + `PerView` AS `per_view`, + `PerViewReplies` AS `per_view_replies`, + `BrowseType` AS `browse_type`, + `IsBrowseSwitch` AS `is_browse_switch`, + `PostFormPosition` AS `post_form_position`, + `NumberOfLevels` AS `number_of_levels`, + `IsDisplaySwitch` AS `is_display_switch`, + `IsRatable` AS `is_ratable`, + `ViewingThreshold` AS `viewing_threshold`, + `IsOn` AS `is_on`, + `RootStylePrefix` AS `root_style_prefix`, + `BaseUrl` AS `base_url`, + `ObjectVote` AS `object_vote`, + `ObjectReaction` AS `object_reaction`, + `ObjectScore` AS `object_score`, + `ObjectReport` AS `object_report`, + `TriggerTable` AS `trigger_table`, + `TriggerFieldId` AS `trigger_field_id`, + `TriggerFieldAuthor` AS `trigger_field_author`, + `TriggerFieldTitle` AS `trigger_field_title`, + `TriggerFieldComments` AS `trigger_field_comments`, + `ClassName` AS `class_name`, + `ClassFile` AS `class_file` + FROM `' . self::$sTableSystems . '`', 'name'); + } + return $GLOBALS[$sKey]; + } + + public static function getGlobalInfo ($iUniqueId) + { + return BxDolDb::getInstance()->getRow("SELECT `ti`.*, `ts`.`Name` AS `system_name` FROM `" . self::$sTableIds . "` AS `ti` LEFT JOIN `" . self::$sTableSystems . "` AS `ts` ON `ti`.`system_id`=`ts`.`ID` WHERE `ti`.`id`=:id LIMIT 1", array( + 'id' => (int)$iUniqueId + )); + } + + public static function getGlobalNumByParams($aParams = []) + { + $sQuery = "SELECT COUNT(*) FROM `" . self::$sTableIds . "` WHERE 1"; + foreach($aParams as $aValue) + $sQuery .= " AND `" . $aValue['key'] ."` " . $aValue['operator'] . " '" . $aValue['value'] . "'"; + + return BxDolDb::getInstance()->getOne($sQuery); + } + + public function init ($iId) + { + if (empty($this->iId) && $iId) + $this->setId($iId); + + $this->addMarkers(array( + 'object_id' => $this->getId(), + 'user_id' => $this->_getAuthorId() + )); + } + + public function setParam($sName, $sValue) + { + $this->_aParams[$sName] = $sValue; + } + + public function isParam($sName) + { + return isset($this->_aParams[$sName]); + } + + public function getParam($sName) + { + return $this->_aParams[$sName]; + } + + public function getId () + { + return $this->_iId; + } + + public function isEnabled () + { + return isset($this->_aSystem['is_on']) && $this->_aSystem['is_on']; + } + + public function getSystemId() + { + return $this->_aSystem['system_id']; + } + + public function getSystemName() + { + return $this->_sSystem; + } + + public function getSystemInfo() + { + return $this->_aSystem; + } + + public function getStorageObjectName() + { + return $this->_getFormObject()->getStorageObjectName(); + } + + public function getTranscoderPreviewName() + { + return $this->_getFormObject()->getTranscoderPreviewName(); + } + + public function getFormObject() + { + return $this->_getFormObject(); + } + + public function getTableNameImages() + { + return $this->_sTableImages; + } + + public function getTableNameImages2Entries() + { + return $this->_sTableImages2Entries; + } + + public function getLanguageKey($sIndex) + { + return isset($this->_aT[$sIndex]) ? $this->_aT[$sIndex] : ''; + } + + public function getMaxLevel() + { + return $this->_iDpMaxLevel; + } + + public function getOrder () + { + return $this->_sOrder; + } + + public function getPerView ($iCmtParentId = 0) + { + return $iCmtParentId == 0 ? $this->_aSystem['per_view'] : $this->_aSystem['per_view_replies']; + } + + public function getStatusAdmin() + { + return $this->isEditAllowedAll() || $this->isRemoveAllowedAll() || $this->isAutoApprove() ? BX_CMT_STATUS_ACTIVE : BX_CMT_STATUS_PENDING; + } + + public function getViewUrl($iCmtId, $bAbsolute = true) + { + if(empty($this->_aSystem['trigger_field_title'])) + return ''; + + $s = BxDolPermalinks::getInstance()->permalink($this->_sViewUrl, [ + 'sys' => $this->_sSystem, + 'id' => $this->_iId, + 'cmt_id' => $iCmtId + ]); + + return $bAbsolute ? bx_absolute_url($s) : $s; + } + + public function getViewText($mixedItem) + { + if(!is_array($mixedItem)) + $mixedItem = $this->getCommentSimple((int)$mixedItem); + + if(empty($mixedItem) || !is_array($mixedItem)) + return ''; + + return $this->_prepareTextForOutput($mixedItem['cmt_text'], (int)$mixedItem['cmt_id']); + } + + public function getBaseUrl() + { + $sUrl = $this->_replaceMarkers($this->_sBaseUrl); + $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl); + if(get_mb_substr($sUrl, 0, 6) != 'http:/' && get_mb_substr($sUrl, 0, 7) != 'https:/') + $sUrl = BX_DOL_URL_ROOT . $sUrl; + return $sUrl; + } + + public function getListUrl() + { + $sBaseUrl = $this->getBaseUrl(); + if(empty($sBaseUrl)) + return ''; + + return $sBaseUrl . $this->getListAnchor(true); + } + + public function getItemUrl($iItemId) + { + $sBaseUrl = $this->getBaseUrl(); + if(empty($sBaseUrl)) + return ''; + + return $sBaseUrl . $this->getItemAnchor($iItemId, true); + } + + public function getListAnchor($bWithHash = false) + { + return ($bWithHash ? '#' : '') . sprintf($this->_sListAnchor, str_replace('_', '-', $this->getSystemName()), $this->getId()); + } + + public function getItemAnchor($iItemId, $bWithHash = false) + { + return ($bWithHash ? '#' : '') . sprintf($this->_sItemAnchor, str_replace('_', '-', $this->getSystemName()), $this->getId(), $iItemId); + } + + public function getAttachments($iCmtId) + { + $aResult = array(); + + if(!$this->isAttachImageEnabled()) + return $aResult; + + $aFiles = $this->_oQuery->getFiles($this->_aSystem['system_id'], $iCmtId); + if(empty($aFiles) || !is_array($aFiles)) + return $aResult; + + $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName()); + $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName()); + + foreach($aFiles as $aFile) { + $bImage = $oTranscoder && $oTranscoder->isMimeTypeSupported($aFile['mime_type']); + + $sPreview = ''; + if($bImage) + $sPreview = $oTranscoder->getFileUrl($aFile['image_id']); + + if(!$sPreview) + $sPreview = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name'])); + + $sUrl = $oStorage->getFileUrlById($aFile['image_id']); + + $aResult[] = array( + 'id' => $aFile['image_id'], + 'src' => $sPreview, + 'src_orig' => $bImage ? $sUrl : '', + 'url' => !$bImage ? $sUrl : '' + ); + } + + return $aResult; + } + + public function getConnectionObject($sType) + { + $sResult = ''; + + switch($sType) { + case BX_CMT_FILTER_FRIENDS: + $sResult = $this->_sConnObjFriends; + break; + case BX_CMT_FILTER_SUBSCRIPTIONS: + $sResult = $this->_sConnObjSubscriptions; + break; + } + + return $sResult; + } + + public function getVoteObject($iEniqId) + { + if(empty($this->_aSystem['object_vote'])) + $this->_aSystem['object_vote'] = 'sys_cmts'; + + $oVote = BxDolVote::getObjectInstance($this->_aSystem['object_vote'], $iEniqId, true, $this->_oTemplate); + if(!$oVote || !$oVote->isEnabled()) + return false; + + return $oVote; + } + + public function getReactionObject($iEniqId) + { + if(empty($this->_aSystem['object_reaction'])) + $this->_aSystem['object_reaction'] = 'sys_cmts_reactions'; + + $oReaction = BxDolVote::getObjectInstance($this->_aSystem['object_reaction'], $iEniqId, true, $this->_oTemplate); + if(!$oReaction || !$oReaction->isEnabled()) + return false; + + return $oReaction; + } + + public function getScoreObject($iEniqId) + { + if(empty($this->_aSystem['object_score'])) + $this->_aSystem['object_score'] = 'sys_cmts'; + + $oScore = BxDolScore::getObjectInstance($this->_aSystem['object_score'], $iEniqId, true, $this->_oTemplate); + if(!$oScore || !$oScore->isEnabled()) + return false; + + return $oScore; + } + + public function getReportObject($iEniqId) + { + if(empty($this->_aSystem['object_report'])) + $this->_aSystem['object_report'] = 'sys_cmts'; + + $oReport = BxDolReport::getObjectInstance($this->_aSystem['object_report'], $iEniqId, true, $this->_oTemplate); + if(!$oReport || !$oReport->isEnabled()) + return false; + + return $oReport; + } + + /** + * Is used as: + * 1. Live Update's session key. + * 2. HTML ID for Live Update's 'New Content' button. + * @return string with ID. + */ + public function getNotificationId() + { + return 'cmts-notification-' . $this->_sSystem . '-' . $this->_iId; + } + + public function getSocketName() + { + return 'cmts_' . $this->_sSystem; + } + + /** + * @deprecated since version 10.0.0-B3 and can be removed in later versions. + */ + public function setTableNameFiles($sTable) + { + $this->_sTableImages = $sTable; + $this->_oQuery->setTableNameFiles($sTable); + } + + /** + * @deprecated since version 10.0.0-B3 and can be removed in later versions. + */ + public function setTableNameFiles2Entries($sTable) + { + $this->_sTableImages2Entries = $sTable; + $this->_oQuery->setTableNameFiles2Entries($sTable); + } + + public function isHtml () + { + return $this->_aSystem['html'] > 0; + } + + public function isRatable () + { + return $this->_aSystem['is_ratable']; + } + + public function isAttachImageEnabled() + { + return true; + } + + public function isAutoApprove() + { + return getParam('sys_cmts_enable_auto_approve') == 'on'; + } + + /** + * set id to operate with votes + */ + public function setId ($iId) + { + if ($iId == $this->getId()) return; + $this->_iId = $iId; + } + + /** + * Add replace markers. + * @param $a array of markers as key => value + * @return true on success or false on error + */ + public function addMarkers ($a) + { + if (empty($a) || !is_array($a)) + return false; + $this->_aMarkers = array_merge ($this->_aMarkers, $a); + return true; + } + + /** + * Database functions + */ + public function getQueryObject () + { + return $this->_oQuery; + } + + public function getCommentsTableName () + { + return $this->_oQuery->getTableName (); + } + + public function getObjectAuthorId ($iObjectId = 0) + { + if(empty($this->_aSystem['trigger_field_author'])) + return 0; + + return $this->_oQuery->getObjectAuthorId ($iObjectId ? $iObjectId : $this->getId()); + } + + public function getObjectTitle ($iObjectId = 0) + { + if(empty($this->_aSystem['trigger_field_title'])) + return ''; + + $sTitle = $this->_oQuery->getObjectTitle ($iObjectId ? $iObjectId : $this->getId()); + if(get_mb_substr($sTitle, 0, 1) == '_') + $sTitle = _t($sTitle); + + return $sTitle; + } + + public function getObjectPrivacyView ($iObjectId = 0) + { + if(empty($iObjectId)) + $iObjectId = $this->getId(); + + $sFieldPrivacyView = ''; + if(!empty($this->_aSystem['trigger_field_privacy_view'])) + $sFieldPrivacyView = $this->_aSystem['trigger_field_privacy_view']; + + if(($iPrivacyView = $this->_oQuery->getObjectPrivacyView($iObjectId, $sFieldPrivacyView)) !== false) + return $iPrivacyView; + + return BX_DOL_PG_ALL; + } + + /* + * Gets Content Filter object. + */ + public function getObjectContentFilter () + { + $oCf = BxDolContentFilter::getInstance(); + if(!$oCf->isEnabledForComments()) + return false; + + return $oCf; + } + + public function getCommentsCountAll ($iObjectId = 0, $bForceCalculate = false) + { + return $this->_oQuery->getCommentsCountAll ($iObjectId ? $iObjectId : $this->getId(), $this->_getAuthorId(), $bForceCalculate); + } + + public function getCommentsCount ($iObjectId = 0, $iCmtVParentId = -1, $sFilter = '') + { + return $this->_oQuery->getCommentsCount ($iObjectId ? $iObjectId : $this->getId(), $iCmtVParentId, $this->_getAuthorId(), $sFilter); + } + + public function getCommentsArray ($iVParentId, $sFilter, $aOrder, $iStart = 0, $iCount = -1) + { + return $this->_oQuery->getComments ($this->getId(), $iVParentId, $this->_getAuthorId(), $sFilter, $aOrder, $iStart, $iCount); + } + + public function getCommentsBy ($aParams = []) + { + return $this->_oQuery->getCommentsBy($aParams); + } + + /** + * Get comment's unique id. + */ + public function getCommentUniqId($iCmtId, $iAuthorId = 0) + { + return $this->_oQuery->getUniqId($this->getSystemId(), $iCmtId, ['author_id' => $iAuthorId]); + } + + /** + * Get comment's short info. + */ + public function getCommentSimple ($iCmtId) + { + return $this->_oQuery->getCommentSimple ($this->getId(), $iCmtId); + } + + /** + * Get comment's full info. + */ + public function getCommentRow ($iCmtId) + { + return $this->_oQuery->getComment ($this->getId(), $iCmtId); + } + + public function getCommentParents ($mixedCmt, $iDepthMax = 0) + { + if(!is_array($mixedCmt)) + $mixedCmt = $this->_oQuery->getCommentsBy(['type' => 'id', 'id' => (int)$mixedCmt]); + + $iDepth = 0; + $aParents = []; + $this->_getParents($mixedCmt, $iDepthMax, $iDepth, $aParents); + + return $aParents; + } + + public function onObjectDelete ($iObjectId = 0) + { + // delete comments + $aFiles = $aCmtIds = []; + $this->_oQuery->deleteObjectComments ($iObjectId ? $iObjectId : $this->getId(), $aFiles, $aCmtIds); + + // delete votes + $this->deleteVotes($aCmtIds); + + // delete reactions + $this->deleteReactions($aCmtIds); + + // delete scores + $this->deleteScores($aCmtIds); + + // delete reports + $this->deleteReports($aCmtIds); + + // delete meta info + $this->deleteMetaInfo($aCmtIds); + + // delete files + if ($aFiles) { + $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName()); + if ($oStorage) + $oStorage->queueFilesForDeletion($aFiles); + } + + // delete unique IDs + $this->deleteUniqueIds($aCmtIds); + } + + public static function onAuthorDelete ($iAuthorId) + { + $aSystems = self::getSystems(); + foreach($aSystems as $sSystem => $aSystem) { + $o = self::getObjectInstance($sSystem, 0); + $oQuery = $o->getQueryObject(); + + // delete comments + $aFiles = $aCmtIds = array (); + $oQuery->deleteAuthorComments($iAuthorId, $aFiles, $aCmtIds); + + // delete votes + $o->deleteVotes($aCmtIds); + + // delete reactions + $o->deleteReactions($aCmtIds); + + // delete scores + $o->deleteScores($aCmtIds); + + // delete reports + $o->deleteReports($aCmtIds); + + // delete meta info + $o->deleteMetaInfo($aCmtIds); + + // delete files + $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName()); + if ($oStorage) + $oStorage->queueFilesForDeletion($aFiles); + + // delete unique IDs + $o->deleteUniqueIds($aCmtIds); + } + return true; + } + + public static function onModuleEnable ($sModuleName) + { + $aSystems = self::getSystems(); + foreach($aSystems as $sSystem => $aSystem) { + if ($sModuleName !== $aSystem['module']) + continue; + + $o = self::getObjectInstance($sSystem, 0); + $o->registerTranscoders(); + } + + return true; + } + + public static function onModuleDisable ($sModuleName) + { + $aSystems = self::getSystems(); + foreach($aSystems as $sSystem => $aSystem) { + if ($sModuleName !== $aSystem['module']) + continue; + + $o = self::getObjectInstance($sSystem, 0); + $o->unregisterTranscoders(); + } + + return true; + } + + public static function onModuleUninstall ($sModuleName, &$iFiles = null) + { + $aSystems = self::getSystems(); + foreach($aSystems as $sSystem => $aSystem) { + if ($sModuleName !== $aSystem['module']) + continue; + + $o = self::getObjectInstance($sSystem, 0); + $oQuery = $o->getQueryObject(); + + // delete comments + $aFiles = $aCmtIds = array (); + $oQuery->deleteAll($aSystem['system_id'], $aFiles, $aCmtIds); + + // delete votes + $o->deleteVotes($aCmtIds); + + // delete reactions + $o->deleteReactions($aCmtIds); + + // delete scores + $o->deleteScores($aCmtIds); + + // delete reports + $o->deleteReports($aCmtIds); + + // delete meta info + $o->deleteMetaInfo($aCmtIds); + + // delete files + $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName()); + if ($oStorage && $aFiles) + $oStorage->queueFilesForDeletion($aFiles); + + if (null !== $iFiles) + $iFiles += count($aFiles); + + // delete unique IDs + $o->deleteUniqueIds($aCmtIds); + } + + return true; + } + + public function deleteVotes ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = [$mixedCmtId]; + + foreach($mixedCmtId as $iCmtId) + if(($oReport = $this->getVoteObject($this->getCommentUniqId($iCmtId))) !== false) + $oReport->onObjectDelete(); + } + + public function deleteReactions ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = [$mixedCmtId]; + + foreach($mixedCmtId as $iCmtId) + if(($oReport = $this->getReactionObject($this->getCommentUniqId($iCmtId))) !== false) + $oReport->onObjectDelete(); + } + + public function deleteScores ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = [$mixedCmtId]; + + foreach($mixedCmtId as $iCmtId) + if(($oReport = $this->getScoreObject($this->getCommentUniqId($iCmtId))) !== false) + $oReport->onObjectDelete(); + } + + public function deleteReports ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = [$mixedCmtId]; + + foreach($mixedCmtId as $iCmtId) + if(($oReport = $this->getReportObject($this->getCommentUniqId($iCmtId))) !== false) + $oReport->onObjectDelete(); + } + + public function deleteMetaInfo ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = array($mixedCmtId); + + $oMetatags = false; + if(!empty($this->_sMetatagsObj)) + $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj); + + if($oMetatags) + foreach($mixedCmtId as $iCmtId) + $oMetatags->onDeleteContent($this->getCommentUniqId($iCmtId)); + } + + public function deleteUniqueIds ($mixedCmtId) + { + if(!is_array($mixedCmtId)) + $mixedCmtId = array($mixedCmtId); + + foreach($mixedCmtId as $iCmtId) + $this->_oQuery->deleteCmtIds($this->_aSystem['system_id'], $iCmtId); + } + + /** + * Permissions functions + */ + public function isAdmin($iCmtAuthorId) + { + return BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR), $iCmtAuthorId); + } + + public function checkAction ($sAction, $isPerformAction = false) + { + $iId = $this->_getAuthorId(); + $a = checkActionModule($iId, $sAction, 'system', $isPerformAction); + return $a[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED; + } + + public function checkActionErrorMsg ($sAction) + { + $iId = $this->_getAuthorId(); + $a = checkActionModule($iId, $sAction, 'system'); + return $a[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $a[CHECK_ACTION_MESSAGE] : ''; + } + + public function isViewAllowed ($isPerformAction = false) + { + if (BxDolRequest::serviceExists($this->_aSystem['module'], 'check_allowed_comments_view')) { + $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_view', array($this->getId(), $this->getSystemName())); + if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED) + return $mixedResult; + } + + return CHECK_ACTION_RESULT_ALLOWED; + } + + public function isVoteAllowed ($aCmt, $isPerformAction = false) + { + if(!$this->isRatable()) + return false; + + $oVote = $this->getVoteObject($this->getCommentUniqId($aCmt['cmt_id'])); + if($oVote === false) + return false; + + $iUserId = (int)$this->_getAuthorId(); + if($iUserId == 0) + return false; + + if(isAdmin()) + return true; + + return $oVote->isAllowedVote($isPerformAction); + } + + public function isScoreAllowed ($aCmt, $isPerformAction = false) + { + if(!$this->isRatable()) + return false; + + $oScore = $this->getScoreObject($this->getCommentUniqId($aCmt['cmt_id'])); + if($oScore === false) + return false; + + $iUserId = (int)$this->_getAuthorId(); + if($iUserId == 0) + return false; + + if(isAdmin()) + return true; + + return $oScore->isAllowedVote($isPerformAction); + } + + public function isReportAllowed ($aCmt, $isPerformAction = false) + { + $oReport = $this->getReportObject($aCmt['cmt_id']); + if($oReport === false) + return false; + + $iUserId = (int)$this->_getAuthorId(); + if($iUserId == 0) + return false; + + if(isAdmin()) + return true; + + return $oReport->isAllowedReport($isPerformAction); + } + + public function isPostAllowed ($isPerformAction = false) + { + if (BxDolRequest::serviceExists($this->_aSystem['module'], 'check_allowed_comments_post')) { + $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_post', array($this->getId(), $this->getSystemName())); + if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED) + return false; + } + + return $this->checkAction ('comments post', $isPerformAction); + } + + public function msgErrPostAllowed () + { + return $this->checkActionErrorMsg('comments post'); + } + + /** + * Determines whether a 'reply' action allowed or not. + * @param integer or array $mixedCmt - ID or an array which describes the comment to be replied + * @param boolean $isPerformAction + * @return boolean + */ + public function isReplyAllowed ($mixedCmt, $isPerformAction = false) + { + return $this->isPostAllowed($isPerformAction); + } + + public function msgErrReplyAllowed () + { + return $this->msgErrPostAllowed (); + } + + public function isQuoteAllowed ($mixedCmt, $isPerformAction = false) + { + return $this->_bPostQuote && $this->isReplyAllowed ($mixedCmt, $isPerformAction); + } + + public function msgErrQuoteAllowed () + { + return $this->msgErrReplyAllowed (); + } + + public function isPinAllowed ($aCmt, $isPerformAction = false) + { + if((int)$aCmt['cmt_parent_id'] != 0 || (int)$aCmt['cmt_pinned'] != 0) + return false; + + if(isAdmin()) + return true; + + return $this->checkAction ('comments pin', $isPerformAction); + } + + public function msgErrPinAllowed () + { + return $this->checkActionErrorMsg('comments pin'); + } + + public function isUnpinAllowed ($aCmt, $isPerformAction = false) + { + if((int)$aCmt['cmt_pinned'] == 0) + return false; + + if(isAdmin()) + return true; + + return $this->checkAction ('comments pin', $isPerformAction); + } + + public function msgErrUnpinAllowed () + { + return $this->checkActionErrorMsg('comments pin'); + } + + public function isEditAllowed ($aCmt, $isPerformAction = false) + { + if(isAdmin()) + return true; + + if($this->isEditAllowedAll($isPerformAction)) + return true; + + $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_post', array($this->getId(), $this->getSystemName())); + if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED) + return false; + + return abs($aCmt['cmt_author_id']) == $this->_getAuthorId() && $this->checkAction ('comments edit own', $isPerformAction); + } + + public function msgErrEditAllowed () + { + return $this->checkActionErrorMsg ('comments edit own'); + } + + public function isEditAllowedAll ($isPerformAction = false) + { + return $this->checkAction('comments edit all', $isPerformAction); + } + + public function isRemoveAllowed ($aCmt, $isPerformAction = false) + { + if(isAdmin()) + return true; + + if($this->isRemoveAllowedAll($isPerformAction)) + return true; + + if(abs($aCmt['cmt_author_id']) == $this->_getAuthorId() && $this->checkAction ('comments remove own', $isPerformAction)) + return true; + + $aContentInfo = BxDolRequest::serviceExists($this->_aSystem['module'], 'get_all') ? BxDolService::call($this->_aSystem['module'], 'get_all', array(array('type' => 'id', 'id' => $this->getId()))) : array(); + $oModule = BxDolModule::getInstance($this->_aSystem['module']); + $CNF = $oModule->_oConfig->CNF; + + if (isset($CNF['FIELD_AUTHOR']) && $aContentInfo[$CNF['FIELD_AUTHOR']] == $this->_getAuthorId() && $this->checkAction('comments remove in own content', $isPerformAction)){ + return true; + } + + if (isset($CNF['FIELD_ALLOW_VIEW_TO']) && $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0){ + $iGroupProfileId = -(int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]; + $oProfileContext = BxDolProfile::getInstance($iGroupProfileId); + $oModule = BxDolModule::getInstance($oProfileContext->getModule()); + if ($oModule->_oConfig->isRoles() && $oModule->getRole($iGroupProfileId, $this->_getAuthorId()) === BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR && $this->checkAction('comments remove in group context', $isPerformAction)){ + return true; + } + } + + return false; + } + + public function isRemoveAllowedAll ($isPerformAction = false) + { + return $this->checkAction ('comments remove all', $isPerformAction); + } + + public function msgErrRemoveAllowed () + { + return $this->checkActionErrorMsg('comments remove own'); + } + + public function isMoreAllowed ($aCmt, $isPerformAction = false) + { + $oMenuManage = BxDolMenu::getObjectInstance($this->_sMenuObjManage); + $oMenuManage->setCmtsData($this, $aCmt['cmt_id']); + return $oMenuManage->isVisible(); + } + + public function isModerator($isPerformAction = false) + { + return $this->isEditAllowedAll($isPerformAction) || $this->isRemoveAllowedAll($isPerformAction); + } + + /** + * Actions functions + */ + public function actionPin () + { + if (!$this->isEnabled()) + return echoJson(array()); + + $iCmtId = 0; + if(bx_get('Cmt') !== false) + $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT); + + $aCmt = $this->getCommentRow($iCmtId); + if(empty($aCmt) || !is_array($aCmt)) + return echoJson(array()); + + $iWay = 0; + if(bx_get('way') !== false) + $iWay = bx_process_input(bx_get('way'), BX_DATA_INT); + + if(!$this->_oQuery->updateComments(array('cmt_pinned' => $iWay ? time() : 0), array('cmt_id' => $iCmtId))) + return echoJson(array()); + + $aBp = $aDp = array(); + $this->_getParams($aBp, $aDp); + $this->_prepareParams($aBp, $aDp); + + echoJson(array( + 'parent_id' => $aCmt['cmt_parent_id'], + 'per_view' => $aBp['per_view'] + )); + } + + public function actionGetFormPost () + { + if (!$this->isEnabled()) + return ''; + + $iCmtParentId = bx_get('CmtParent'); + $iCmtParentId = $iCmtParentId !== false ? bx_process_input($iCmtParentId, BX_DATA_INT) : 0; + + $sCmtBrowse = bx_get('CmtBrowse'); + $sCmtBrowse = $sCmtBrowse !== false ? bx_process_input($sCmtBrowse, BX_DATA_TEXT) : ''; + + $sCmtDisplay = bx_get('CmtDisplay'); + $sCmtDisplay = $sCmtDisplay !== false ? bx_process_input($sCmtDisplay, BX_DATA_TEXT) : ''; + + $bQuote = bx_get('CmtQuote'); + $bQuote = $bQuote !== false ? bx_process_input($bQuote, BX_DATA_INT) == 1 : false; + + $bMinPostForm = bx_get('CmtMinPostForm'); + $bMinPostForm = $bMinPostForm !== false ? bx_process_input($bMinPostForm, BX_DATA_INT) == 1 : $this->_bMinPostForm; + + return $this->getFormBoxPost(array('parent_id' => $iCmtParentId, 'type' => $sCmtBrowse), array('type' => $sCmtDisplay, 'dynamic_mode' => true, 'quote' => $bQuote, 'min_post_form' => $bMinPostForm)); + } + + public function actionGetFormEdit () + { + if (!$this->isEnabled()) + return echoJson(array()); + + $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT); + echoJson($this->getFormEdit($iCmtId, array('dynamic_mode' => true))); + } + + public function actionGetCmt () + { + if(!$this->isEnabled()) + return ''; + + if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED) + return ''; + + $iCmtId = bx_process_input($_REQUEST['Cmt'], BX_DATA_INT); + $sCmtBrowse = isset($_REQUEST['CmtBrowse']) ? bx_process_input($_REQUEST['CmtBrowse'], BX_DATA_TEXT) : ''; + $sCmtDisplay = isset($_REQUEST['CmtDisplay']) ? bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT) : ''; + + $aCmt = $this->getCommentRow($iCmtId); + echoJson([ + 'parent_id' => $aCmt['cmt_parent_id'], + 'vparent_id' => $aCmt['cmt_parent_id'], + 'content' => $this->getComment($aCmt, ['type' => $sCmtBrowse], ['type' => $sCmtDisplay, 'dynamic_mode' => true]) + ]); + } + + public function actionGetCmts () + { + if (!$this->isEnabled()) + return ''; + + if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED) + return ''; + + $aBp = $aDp = array(); + $this->_getParams($aBp, $aDp); + + $aDp['dynamic_mode'] = true; + + //--- Beg: Using pregenerated structure + if(isset($aDp['structure']) && $aDp['structure'] === true) { + $iCmtId = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0; + + $mixedStructure = $this->getCommentStructure($iCmtId, $aBp, $aDp); + if($mixedStructure === false || empty($mixedStructure[$iCmtId]['items'])) + return ''; + + $aDp['structure'] = $mixedStructure[$iCmtId]['items']; + return $this->getCommentsByStructure($aBp, $aDp); + } + //--- End: Using pregenerated structure + else + return $this->{'getComments' . ($aBp['pinned'] ? 'Pinned' : '')}($aBp, $aDp); + } + + public function actionSubmitPostForm() + { + if(!$this->isEnabled()) + return echoJson(array()); + + $iCmtParentId = 0; + if(bx_get('cmt_parent_id') !== false) + $iCmtParentId = bx_process_input(bx_get('cmt_parent_id'), BX_DATA_INT); + + echoJson($this->getFormPost($iCmtParentId, array('dynamic_mode' => true))); + } + + public function actionSubmitEditForm() + { + if (!$this->isEnabled()) { + echoJson(array()); + return; + }; + + $iCmtId = 0; + if(bx_get('cmt_id') !== false) + $iCmtId = bx_process_input(bx_get('cmt_id'), BX_DATA_INT); + + echoJson($this->getFormEdit($iCmtId, array('dynamic_mode' => true))); + } + + public function actionRemove() + { + if (!$this->isEnabled()) + return echoJson([]); + + $iCmtId = 0; + if(bx_get('Cmt') !== false) + $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT); + + echoJson($this->remove($iCmtId)); + } + + public function remove($iCmtId) + { + $aCmt = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iCmtId)); + if(!$aCmt) + return array('msg' => _t('_No such comment')); + + $iObjId = $this->getId(); + if(!$iObjId){ + $this->setId($aCmt['cmt_object_id']); + $iObjId = $aCmt['cmt_object_id']; + } + $iObjAthrId = $this->getObjectAuthorId($iObjId); + $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId); + + if($aCmt['cmt_replies'] > 0) { + if(!$this->isModerator()) + return array('msg' => _t('_Can not delete comments with replies')); + + $aReplies = $this->_oQuery->getCommentsBy(array('type' => 'parent_id', 'parent_id' => $iCmtId)); + foreach($aReplies as $aReply) + $this->remove($aReply['cmt_id']); + } + + $iPerformerId = $this->_getAuthorId(); + if(!$this->isRemoveAllowed($aCmt)) + return array('msg' => $aCmt['cmt_author_id'] == $iPerformerId ? strip_tags($this->msgErrRemoveAllowed()) : _t('_Access denied')); + + $iCmtPrntId = (int)$aCmt['cmt_parent_id']; + if(!$this->_oQuery->removeComment($iObjId, $iCmtId, $iCmtPrntId)) + return array('msg' => _t('_cmt_err_cannot_perform_action')); + + $this->_triggerComment(); + + $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName()); + + $aImages = $this->_oQuery->getFiles($this->_aSystem['system_id'], $iCmtId); + foreach($aImages as $aImage) + $oStorage->deleteFile($aImage['image_id']); + + $this->_oQuery->deleteImages($this->_aSystem['system_id'], $iCmtId); + + $this->isRemoveAllowed($aCmt, true); + + $this->deleteVotes($iCmtId); + + $this->deleteReactions($iCmtId); + + $this->deleteScores($iCmtId); + + $this->deleteReports($iCmtId); + + $this->deleteMetaInfo($iCmtId); + + $aAuditParams = $this->_prepareAuditParams($iCmtId, array('comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text'])); + bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_delete_comment', $aAuditParams); + + $aAlertParams = $this->_prepareAlertParams($aCmt); + + /** + * @hooks + * @hookdef hook-bx_dol_comment-commentRemoved '{object_name}', 'commentRemoved' - hook after a comment was removed + * - $unit_name - comment object name + * - $action - equals `commentRemoved` + * - $object_id - commented object id + * - $sender_id - profile id who performed the action + * - $extra_params - array of additional params with the following array keys: + * - `source` - [string] unique comment source string + * - `object_system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `object_author_id` - [int] commented object author profile id + * - `comment_id` - [int] comment id unique in the comment object scope + * - `comment_uniq_id` - [int] system wide unique comment id + * - `comment_author_id` - [int] comment author profile id + * - `comment_text` - [string] comment text + * - `privacy_view` - [int] or [string] privacy for view comment action, @see BxDolPrivacy + * - `cf` - [int] comment's audience filter value + * @hook @ref hook-bx_dol_comment-commentRemoved + */ + bx_alert($this->_sSystem, 'commentRemoved', $iObjId, $iPerformerId, $aAlertParams); + + /** + * @hooks + * @hookdef hook-comment-deleted 'comment', 'deleted' - hook after a comment was removed + * It's equivalent to @ref hook-bx_dol_comment-commentRemoved + * except 'comment id' is provided in $object_id + * @hook @ref hook-comment-deleted + */ + bx_alert('comment', 'deleted', $iCmtId, $iPerformerId, $aAlertParams); + + if(!empty($iCmtPrntId)) { + $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId); + if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) { + $aAlertParamsReply = $this->_prepareAlertParamsReply($aCmt, $aCmtPrnt); + + /** + * @hooks + * @hookdef hook-bx_dol_comment-replyRemoved '{object_name}', 'replyRemoved' - hook after a reply was removed + * - $unit_name - comment object name + * - $action - equals `replyRemoved` + * - $object_id - commented object id + * - $sender_id - profile id who performed the action + * - $extra_params - array of additional params with the following array keys: + * - `source` - [string] unique comment source string + * - `object_system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `object_author_id` - [int] commented object author profile id + * - `object_author_id` - [int] commented object author profile id + * - `parent_id` - [int] parent comment id unique in the comment object scope + * - `parent_uniq_id` - [int] system wide unique parent comment id + * - `parent_author_id` - [int] parent comment author profile id + * - `comment_id` - [int] comment id unique in the comment object scope + * - `comment_uniq_id` - [int] system wide unique comment id + * - `comment_author_id` - [int] comment author profile id + * - `comment_text` - [string] comment text + * - `privacy_view` - [int] or [string] privacy for view comment action, @see BxDolPrivacy + * @hook @ref hook-bx_dol_comment-replyRemoved + */ + bx_alert($this->_sSystem, 'replyRemoved', $iCmtPrntId, $iPerformerId, $aAlertParamsReply); + + /** + * @hooks + * @hookdef hook-reply-deleted 'reply', 'deleted' - hook after a reply was removed + * It's equivalent to @ref hook-bx_dol_comment-replyRemoved + * except 'comment id' is provided in $object_id + * @hook @ref hook-reply-deleted + */ + bx_alert('reply', 'deleted', $iCmtId, $iPerformerId, $aAlertParamsReply); + } + } + + $this->deleteUniqueIds($iCmtId); + + $iCount = (int)$this->getCommentsCountAll(0, true); + return [ + 'id' => $iCmtId, + 'count' => $iCount, + 'countf' => $iCount > 0 ? $this->getCounter() : '' + ]; + } + + public function add($aValues) + { + return $this->_getFormAdd($aValues); + } + + public function actionResumeLiveUpdate() + { + $sKey = $this->getNotificationId(); + + bx_import('BxDolSession'); + BxDolSession::getInstance()->unsetValue($sKey); + } + + public function actionPauseLiveUpdate() + { + $sKey = $this->getNotificationId(); + + bx_import('BxDolSession'); + BxDolSession::getInstance()->setValue($sKey, 1); + } + + public function actionGetSiblingFiles() + { + $iFileId = 0; + if(bx_get('file_id') !== false) + $iFileId = bx_process_input(bx_get('file_id'), BX_DATA_INT); + + if(!($aFileInfo = $this->_oQuery->getFileInfoById((int)$iFileId))) + return echoJson(['error' => _t('_sys_txt_error_occured')]); + + $aSiblings = []; + $aFiles = $this->_oQuery->getFiles($aFileInfo['system_id'], $aFileInfo['cmt_id']); + foreach($aFiles as $iIndex => $aFile) { + if($aFile['id'] != $iFileId) + continue; + + $aSiblings['prev'] = isset($aFiles[$iIndex - 1]) ? $aFiles[$iIndex - 1] : false; + $aSiblings['next'] = isset($aFiles[$iIndex + 1]) ? $aFiles[$iIndex + 1] : false; + } + + $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName()); + $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName()); + + foreach($aSiblings as $sSibling => $aSibling) { + if(!$aSibling) + continue; + + $sFile = ''; + if($oTranscoder && $oTranscoder->isMimeTypeSupported($aSibling['mime_type'])) + $sFile = $oStorage->getFileUrlById($aSibling['image_id']); + else + $sFile = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name'])); + + $aImageInfo = BxDolImageResize::getInstance()->getImageSize($sFile); + + $iWidth = $iHeight = 0; + if(isset($aImageInfo['w'])) + $iWidth = (int)$aImageInfo['w']; + if(isset($aImageInfo['h'])) + $iHeight = (int)$aImageInfo['h']; + + $aSiblings[$sSibling] = array_merge($aSibling, [ + 'file' => $sFile, + 'file_id' => $aSibling['id'], + 'w' => $iWidth, + 'h' => $iHeight + ]); + } + + return echoJson($aSiblings); + } + + public function onPostAfter($iCmtId, $aDp = []) + { + $iObjId = (int)$this->getId(); + + $aCmt = $this->_oQuery->getCommentSimple($iObjId, $iCmtId); + if(empty($aCmt) || !is_array($aCmt)) + return false; + + $iCmtPrntId = (int)$aCmt['cmt_parent_id']; + $iPerformerId = (int)$aCmt['cmt_author_id']; + + $aAlertParams = $this->_prepareAlertParams($aCmt); + + /** + * @hooks + * @hookdef hook-bx_dol_comment-commentPost '{object_name}', 'commentPost' - hook after a comment was added + * - $unit_name - comment object name + * - $action - equals `commentPost` + * - $object_id - commented object id + * - $sender_id - profile id who performed the action + * - $extra_params - array of additional params with the following array keys: + * - `source` - [string] unique comment source string + * - `object_system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `object_author_id` - [int] commented object author profile id + * - `comment_id` - [int] comment id unique in the comment object scope + * - `comment_uniq_id` - [int] system wide unique comment id + * - `comment_author_id` - [int] comment author profile id + * - `comment_text` - [string] comment text + * - `privacy_view` - [int] or [string] privacy for view comment action, @see BxDolPrivacy + * - `cf` - [int] comment's audience filter value + * @hook @ref hook-bx_dol_comment-commentPost + */ + bx_alert($this->_sSystem, 'commentPost', $iObjId, $iPerformerId, $aAlertParams); + + /** + * @hooks + * @hookdef hook-comment-added 'comment', 'added' - hook after a comment was added + * It's equivalent to @hook @ref hook-bx_dol_comment-commentPost + * except 'comment id' is provided in $object_id + * @hook @ref hook-comment-added + */ + bx_alert('comment', 'added', $iCmtId, $iPerformerId, $aAlertParams); + + $aAuditParams = $this->_prepareAuditParams($iCmtId, array('comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text'])); + bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_add_comment', $aAuditParams); + + if(!empty($iCmtPrntId)) { + $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId); + if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) { + $aAlertParamsReply = $this->_prepareAlertParamsReply($aCmt, $aCmtPrnt); + + /** + * @hooks + * @hookdef hook-bx_dol_comment-replyPost '{object_name}', 'replyPost' - hook after a reply was added + * - $unit_name - comment object name + * - $action - equals `replyPost` + * - $object_id - parent comment id + * - $sender_id - profile id who performed the action + * - $extra_params - array of additional params with the following array keys: + * - `source` - [string] unique comment source string + * - `object_system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `object_author_id` - [int] commented object author profile id + * - `object_author_id` - [int] commented object author profile id + * - `parent_id` - [int] parent comment id unique in the comment object scope + * - `parent_uniq_id` - [int] system wide unique parent comment id + * - `parent_author_id` - [int] parent comment author profile id + * - `comment_id` - [int] comment id unique in the comment object scope + * - `comment_uniq_id` - [int] system wide unique comment id + * - `comment_author_id` - [int] comment author profile id + * - `comment_text` - [string] comment text + * - `privacy_view` - [int] or [string] privacy for view comment action, @see BxDolPrivacy + * @hook @ref hook-bx_dol_comment-replyPost + */ + bx_alert($this->_sSystem, 'replyPost', $iCmtPrntId, $iPerformerId, $aAlertParamsReply); + + /** + * @hooks + * @hookdef hook-comment-replied 'comment', 'replied' - hook after a reply was added + * It's equivalent to @ref hook-bx_dol_comment-replyPost + * except 'comment id' is provided in $object_id + * @hook @ref hook-comment-replied + */ + bx_alert('comment', 'replied', $iCmtId, $iPerformerId, $aAlertParamsReply); + } + } + + $iCount = (int)$this->getCommentsCountAll(0, true); + $aResult = [ + 'id' => $iCmtId, + 'parent_id' => $iCmtPrntId, + 'count' => $iCount, + 'countf' => $iCount > 0 ? $this->getCounter(['show_script' => false]) : '' + ]; + + if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()) + $oSockets->sendEvent($this->getSocketName(), $iObjId, 'comment_added', json_encode(array_merge($aResult, [ + 'author_id' => $iPerformerId, + 'anchor' => $this->getItemAnchor($iCmtId) + ]))); + + return $aResult; + } + + public function onEditAfter($iCmtId, $aDp = []) + { + $aCmt = $this->getCommentRow($iCmtId); + if(empty($aCmt) || !is_array($aCmt)) + return false; + + $aBp = []; + $this->_getParams($aBp, $aDp); + + $iObjId = (int)$this->getId(); + $iPerformerId = $this->_getAuthorId(); + + $aAlertParams = $this->_prepareAlertParams($aCmt); + /** + * @hooks + * @hookdef hook-bx_dol_comment-commentUpdated '{object_name}', 'commentUpdated' - hook after a comment was updated (edited) + * - $unit_name - comment object name + * - $action - equals `commentUpdated` + * - $object_id - commented object id + * - $sender_id - profile id who performed the action + * - $extra_params - array of additional params with the following array keys: + * - `source` - [string] unique comment source string + * - `object_system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `object_author_id` - [int] commented object author profile id + * - `comment_id` - [int] comment id unique in the comment object scope + * - `comment_uniq_id` - [int] system wide unique comment id + * - `comment_author_id` - [int] comment author profile id + * - `comment_text` - [string] comment text + * - `privacy_view` - [int] or [string] privacy for view comment action, @see BxDolPrivacy + * - `cf` - [int] comment's audience filter value + * @hook @ref hook-bx_dol_comment-commentUpdated + */ + bx_alert($this->_sSystem, 'commentUpdated', $iObjId, $iPerformerId, $aAlertParams); + + /** + * @hooks + * @hookdef hook-comment-edited 'comment', 'edited' - hook after a comment was updated (edited) + * It's equivalent to @ref hook-bx_dol_comment-commentUpdated + * except 'comment id' is provided in $object_id + * @hook @ref hook-comment-edited + */ + bx_alert('comment', 'edited', $iCmtId, $iPerformerId, $aAlertParams); + + $aAuditParams = $this->_prepareAuditParams($iCmtId, ['comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text']]); + bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_edit_comment', $aAuditParams); + + return ['id' => $iCmtId, 'content' => $this->_getContent($aCmt, $aBp, $aDp)]; + } + + public function serviceGetAuthor ($iContentId) + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + $this->setId($aComment['cmt_object_id']); + + return $aComment['cmt_author_id']; + } + + public function serviceGetDateAdded ($iContentId) + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + $this->setId($aComment['cmt_object_id']); + + return $aComment['cmt_time']; + } + + public function serviceGetDateChanged ($iContentId) + { + return 0; + } + public function serviceGetLink ($iContentId) + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + $this->setId($aComment['cmt_object_id']); + + return $this->getViewUrl($iContentId); + } + + public function serviceGetTitle ($iContentId) + { + return ''; + } + + public function serviceGetText ($iContentId) + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + $this->setId($aComment['cmt_object_id']); + + return $aComment['cmt_text']; + } + + public function serviceGetThumb ($iContentId) + { + return ''; + } + + public function serviceGetInfo ($iContentId, $bSearchableFieldsOnly = true) + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + $this->setId($aComment['cmt_object_id']); + + return BxDolContentInfo::formatFields($aComment); + } + + public function serviceGetInfoApi ($iContentId, $bExtendedUnits = false) + { + $aData = $this->serviceGetInfo($iContentId, false); + if($aData) + $aData = $this->getDataAPI($aData, ['extended' => $bExtendedUnits]); + + return $aData; + } + + public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '') + { + $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId)); + if(empty($aComment) || !is_array($aComment)) + return ''; + + $this->setId($aComment['cmt_object_id']); + + return $this->getComment($aComment, array(), array('type' => BX_CMT_DISPLAY_FLAT, 'view_only' => true)); + } + + public function serviceGetAll ($aParams = array()) + { + if(empty($aParams) || !is_array($aParams)) + $aParams = array('type' => 'all'); + + return $this->_oQuery->getCommentsBy($aParams); + } + + public function serviceGetSearchableFieldsExtended($aInputsAdd = array()) + { + $oForm = BxDolForm::getObjectInstance('sys_comment', 'sys_comment_post', $this->_oTemplate); + if(!$oForm) + return array(); + + $aSrchNamesExcept = array(); + $aSrchCaptionsSystem = array( + 'cmt_author_id' => '_sys_form_comment_input_caption_system_cmt_author_id', + 'cmt_text' => '_sys_form_comment_input_caption_system_cmt_text' + ); + $aSrchCaptions = array( + 'cmt_author_id' => '_sys_form_comment_input_caption_cmt_author_id', + 'cmt_text' => '_sys_form_comment_input_caption_cmt_text' + ); + + $aResult = array( + 'cmt_author_id' => array( + 'type' => 'text_auto', + 'caption_system' => $aSrchCaptionsSystem['cmt_author_id'], + 'caption' => $aSrchCaptions['cmt_author_id'], + 'info' => '', + 'value' => '', + 'values' => '', + 'pass' => '' + ) + ); + + $aInputs = $oForm->aInputs; + if(!empty($aInputsAdd) && is_array($aInputsAdd)) + $aInputs = array_merge($aInputs, $aInputsAdd); + + foreach($aInputs as $aInput) + if(in_array($aInput['type'], BxDolSearchExtended::$SEARCHABLE_TYPES) && !in_array($aInput['name'], $aSrchNamesExcept)) + $aResult[$aInput['name']] = array( + 'type' => $aInput['type'], + 'caption_system' => !empty($aInput['caption_system_src']) ? $aInput['caption_system_src'] : '', + 'caption' => !empty($aInput['caption_src']) ? $aInput['caption_src'] : (!empty($aSrchCaptions[$aInput['name']]) ? $aSrchCaptions[$aInput['name']] : ''), + 'info' => !empty($aInput['info_src']) ? $aInput['info_src'] : '', + 'value' => !empty($aInput['value']) ? $aInput['value'] : '', + 'values' => !empty($aInput['values_src']) ? $aInput['values_src'] : '', + 'pass' => !empty($aInput['db']['pass']) ? $aInput['db']['pass'] : '' + ); + + return $aResult; + } + + /** + * Overwrite this method and register transcoder(s) if comments object uses custom transcoder(s), + * which differs from default one 'sys_cmts_images_preview' + */ + public function registerTranscoders() + {} + + /** + * Overwrite this method and unregister transcoder(s) if comments object uses custom transcoder(s), + * which differs from default one 'sys_cmts_images_preview' + */ + public function unregisterTranscoders() + {} + + public function serviceGetSearchResultExtended($aParams, $iStart = 0, $iPerPage = 0, $bFilterMode = false) + { + if((empty($aParams) || !is_array($aParams)) && !$bFilterMode) + return array(); + + return $this->_oQuery->getCommentsBy(array('type' => 'search_ids', 'search_params' => $aParams, 'start' => $iStart, 'per_page' => $iPerPage)); + } + + public function getAuthorInfo($iAuthorId = 0) + { + return $this->_getAuthorInfo($iAuthorId); + } + + public function getParams(&$aBp, &$aDp) + { + return $this->_getParams($aBp, $aDp); + } + + public function prepareParams(&$aBp, &$aDp) + { + return $this->_prepareParams($aBp, $aDp); + } + + /** + * Internal functions + */ + protected function _getAuthorId () + { + return isMember() ? bx_get_logged_profile_id() : 0; + } + + protected function _getAuthorPassword () + { + return getLoggedPassword(); + } + + protected function _getAuthorIp () + { + return getVisitorIP(); + } + + protected function _getAuthorInfo($iAuthorId = 0) + { + $iUserId = $this->_getAuthorId(); + $iAuthorId = (int)$iAuthorId; + $oProfile = $this->_getAuthorObject($iAuthorId); + if (!$oProfile->isActive() && !isAdmin() && !BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR))) + $oProfile = BxDolProfileUndefined::getInstance(); + + return array( + $oProfile->getDisplayName(), + $oProfile->getUrl(), + $oProfile->getThumb(), + $oProfile->getUnit(0, array('template' => 'unit_wo_info')), + $oProfile->getBadges() + ); + } + + protected function _getAuthorObject($iAuthorId = 0) + { + return BxDolProfile::getInstanceMagic((int)$iAuthorId); + } + + protected function _getFormObject($sAction = BX_CMT_ACTION_POST) + { + $sDisplayName = '_sFormDisplay' . ucfirst($sAction); + + return BxDolForm::getObjectInstance($this->_sFormObject, $this->$sDisplayName, false, $this->_sSystem); + } + + protected function _unsetFormObject($sAction = BX_CMT_ACTION_POST) + { + $sDisplayName = '_sFormDisplay' . ucfirst($sAction); + + return BxDolForm::unsetObjectInstance($this->_sFormObject, $this->$sDisplayName, false, $this->_sSystem); + } + + protected function _getParams(&$aBp, &$aDp) + { + //--- Process 'Browse' params. + $aBp['parent_id'] = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0; + + $aBp['vparent_id'] = isset($aBp['vparent_id']) ? (int)$aBp['vparent_id'] : 0; + if(bx_get('CmtParent') !== false) + $aBp['vparent_id'] = bx_process_input(bx_get('CmtParent'), BX_DATA_INT); + + $aBp['type'] = isset($aBp['type']) ? $aBp['type'] : ''; + if(bx_get('CmtBrowse') !== false) + $aBp['type'] = bx_process_input(bx_get('CmtBrowse'), BX_DATA_TEXT); + + $aBp['filter'] = isset($aBp['filter']) ? $aBp['filter'] : ''; + if(bx_get('CmtFilter') !== false) + $aBp['filter'] = bx_process_input(bx_get('CmtFilter'), BX_DATA_TEXT); + + $aBp['start'] = isset($aBp['start']) ? (int)$aBp['start'] : -1; + if(bx_get('CmtStart') !== false) + $aBp['start'] = bx_process_input($_REQUEST['CmtStart'], BX_DATA_INT); + + $aBp['per_view'] = isset($aBp['per_view']) ? (int)$aBp['per_view'] : -1; + if(bx_get('CmtPerView') !== false) + $aBp['per_view'] = bx_process_input($_REQUEST['CmtPerView'], BX_DATA_INT); + + $aBp['pinned'] = isset($aBp['pinned']) ? (int)$aBp['pinned'] : 0; + if(bx_get('CmtPinned') !== false) + $aBp['pinned'] = bx_process_input(bx_get('CmtPinned'), BX_DATA_INT); + + //--- Process 'Display' params. + $aDp['type'] = isset($aDp['type']) ? $aDp['type'] : ''; + if(bx_get('CmtDisplay') !== false) + $aDp['type'] = bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT); + + if(bx_get('CmtDisplayStructure') !== false) { + $aDp['structure'] = bx_process_input($_REQUEST['CmtDisplayStructure'], BX_DATA_INT) == 1; + + if($aDp['structure'] && bx_get('CmtParent') !== false) + $aBp['parent_id'] = bx_process_input(bx_get('CmtParent'), BX_DATA_INT); + } + + $aDp['blink'] = isset($aDp['blink']) ? $aDp['blink'] : ''; + if(bx_get('CmtBlink') !== false) + $aDp['blink'] = bx_process_input($_REQUEST['CmtBlink'], BX_DATA_TEXT); + + $aDp['quote'] = isset($aDp['quote']) ? (int)$aDp['quote'] : 0; + if(bx_get('CmtQuote') !== false) + $aDp['quote'] = bx_process_input($_REQUEST['CmtQuote'], BX_DATA_INT); + + $aDp['min_post_form'] = isset($aDp['min_post_form']) ? (bool)$aDp['min_post_form'] : $this->_bMinPostForm; + if(bx_get('CmtMinPostForm') !== false) + $aDp['min_post_form'] = bx_process_input(bx_get('CmtMinPostForm'), BX_DATA_INT) == 1; + + $aDp['in_designbox'] = isset($aDp['in_designbox']) ? (bool)$aDp['in_designbox'] : true; + $aDp['dynamic_mode'] = isset($aDp['dynamic_mode']) ? (bool)$aDp['dynamic_mode'] : false; + $aDp['show_empty'] = isset($aDp['show_empty']) ? (bool)$aDp['show_empty'] : false; + } + + protected function _prepareAlertParams($aCmt) + { + $iObjId = (int)$this->getId(); + $iObjAthrId = $this->getObjectAuthorId($iObjId); + $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId); + + $iCmtId = (int)$aCmt['cmt_id']; + $iCmtUniqId = $this->getCommentUniqId($iCmtId); + $iCmtCf = isset($aCmt['cmt_cf']) ? (int)$aCmt['cmt_cf'] : BxDolContentFilter::getInstance()->getDefaultValue(); + + return array( + 'source' => 'sys_cmts_' . $iCmtUniqId, + + 'object_system' => $this->_sSystem, + 'object_id' => $iObjId, + 'object_author_id' => $iObjAthrId, + + 'comment_id' => $iCmtId, + 'comment_uniq_id' => $iCmtUniqId, + 'comment_author_id' => $aCmt['cmt_author_id'], + 'comment_text' => $aCmt['cmt_text'], + + 'privacy_view' => $iObjAthrPrivacyView, + 'cf' => $iCmtCf + ); + } + + protected function _prepareAlertParamsReply($aCmt, $aCmtPrnt) + { + $iObjId = (int)$this->getId(); + $iObjAthrId = $this->getObjectAuthorId($iObjId); + $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId); + + $iCmtPrntId = (int)$aCmt['cmt_parent_id']; + $iCmtPrntUniqId = $this->getCommentUniqId($iCmtPrntId); + + $iCmtId = (int)$aCmt['cmt_id']; + $iCmtUniqId = $this->getCommentUniqId($iCmtId); + + return array( + 'source' => 'sys_cmts_' . $iCmtUniqId, + + 'object_system' => $this->_sSystem, + 'object_id' => $iObjId, + 'object_author_id' => $iObjAthrId, + + 'parent_id' => $iCmtPrntId, + 'parent_uniq_id' => $iCmtPrntUniqId, + 'parent_author_id' => $aCmtPrnt['cmt_author_id'], + + 'comment_id' => $iCmtId, + 'comment_uniq_id' => $iCmtUniqId, + 'comment_author_id' => $aCmt['cmt_author_id'], + 'comment_text' => $aCmt['cmt_text'], + + 'privacy_view' => $iObjAthrPrivacyView, + ); + } + + protected function _prepareAuditParams($iId, $aData) + { + $sModule = $this->_aSystem['module']; + $oModule = BxDolModule::getInstance($sModule); + $CNF = isset($oModule->_oConfig->CNF) ? $oModule->_oConfig->CNF : array(); + + $aContentInfo = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $this->getId()))) : array(); + $iContextId = 0; + if (!empty($aContentInfo)){ + $iContextId = isset($CNF['FIELD_ALLOW_VIEW_TO']) && (!empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) ? - $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] : 0; + } + + $AuditParams = array( + 'content_title' => $this->getObjectTitle() , + 'context_profile_id' => $iContextId, + 'content_info_object' => isset($CNF['OBJECT_CMTS_CONTENT_INFO']) ? $CNF['OBJECT_CMTS_CONTENT_INFO'] : '', + 'data' => array_merge(array('comment_id' => $iId), $aData) + ); + if ($iContextId > 0) + $AuditParams['context_profile_title'] = BxDolProfile::getInstance($iContextId)->getDisplayName(); + + return $AuditParams; + } + + protected function _prepareTextForOutput ($s, $iCmtId = 0) + { + $iDataAction = !$this->isHtml() ? BX_DATA_TEXT_MULTILINE : BX_DATA_HTML; + $s = bx_process_output($s, $iDataAction); + $s = bx_linkify_html($s, 'class="' . BX_DOL_LINK_CLASS . '"'); + + if($iCmtId && $this->_sMetatagsObj && ($oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj)) !== false) + $s = $oMetatags->metaParse($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId), $s); + + return $s; + } + + protected function _prepareStructureBp($sDType, &$aBp) + { + $aBp['type'] = !empty($aBp['type']) ? $aBp['type'] : $this->_sBrowseType; + $aBp['filter'] = !empty($aBp['filter']) ? $aBp['filter'] : $this->_sBrowseFilter; + $aBp['parent_id'] = isset($aBp['parent_id']) ? $aBp['parent_id'] : 0; + $aBp['start'] = isset($aBp['start']) ? $aBp['start'] : -1; + $aBp['init_view'] = isset($aBp['init_view']) ? $aBp['init_view'] : -1; + $aBp['per_view'] = isset($aBp['per_view']) ? $aBp['per_view'] : -1; + $aBp['order']['by'] = isset($aBp['order_by']) ? $aBp['order_by'] : $this->_aOrder['by']; + $aBp['order']['way'] = isset($aBp['order_way']) ? $aBp['order_way'] : $this->_aOrder['way']; + + if($aBp['per_view'] == -1) + switch($sDType) { + case BX_CMT_DISPLAY_FLAT: + $aBp['per_view'] = $this->getPerView(0); + break; + + case BX_CMT_DISPLAY_THREADED: + $aBp['per_view'] = $this->getPerView($aBp['parent_id']); + break; + } + } + + protected function _prepareParams(&$aBp, &$aDp) + { + $aBp['type'] = !empty($aBp['type']) ? $aBp['type'] : $this->_sBrowseType; + $aBp['filter'] = !empty($aBp['filter']) ? $aBp['filter'] : $this->_sBrowseFilter; + $aBp['parent_id'] = isset($aBp['parent_id']) ? $aBp['parent_id'] : 0; + $aBp['start'] = isset($aBp['start']) ? $aBp['start'] : -1; + $aBp['init_view'] = isset($aBp['init_view']) ? $aBp['init_view'] : -1; + $aBp['per_view'] = isset($aBp['per_view']) ? $aBp['per_view'] : -1; + $aBp['pinned'] = isset($aBp['pinned']) ? (int)$aBp['pinned'] : 0; + $aBp['order']['by'] = isset($aBp['order_by']) ? $aBp['order_by'] : $this->_aOrder['by']; + $aBp['order']['way'] = isset($aBp['order_way']) ? $aBp['order_way'] : $this->_aOrder['way']; + + $aDp['type'] = !empty($aDp['type']) ? $aDp['type'] : $this->_sDisplayType; + $aDp['blink'] = !empty($aDp['blink']) ? $aDp['blink'] : array(); + if(!is_array($aDp['blink'])) + $aDp['blink'] = explode(',', $aDp['blink']); + $aDp['quote'] = !empty($aDp['quote']) ? (int)$aDp['quote'] : 0; + if(!isset($aDp['min_post_form'])) + $aDp['min_post_form'] = $this->_bMinPostForm; + + switch($aDp['type']) { + case BX_CMT_DISPLAY_FLAT: + $aBp['vparent_id'] = -1; + $aBp['per_view'] = $aBp['per_view'] != -1 ? $aBp['per_view'] : $this->getPerView(0); + break; + + case BX_CMT_DISPLAY_THREADED: + $iParent = 0; + if(isset($aBp['vparent_id'])) + $iParent = $aBp['vparent_id']; + else if(isset($aBp['parent_id'])) + $iParent = $aBp['parent_id']; + + $aBp['per_view'] = $aBp['per_view'] != -1 ? $aBp['per_view'] : $this->getPerView($iParent); + break; + } + + switch ($aBp['type']) { + case BX_CMT_BROWSE_POPULAR: + $aBp['order'] = array( + 'by' => BX_CMT_ORDER_BY_POPULAR, + 'way' => BX_CMT_ORDER_WAY_DESC + ); + break; + } + + if(!isset($aBp['count'])) + $aBp['count'] = $this->getCommentsCount($this->_iId, $aBp['vparent_id'], $aBp['filter']); + + if($aBp['start'] != -1) + return; + + $aBp['start'] = 0; + if($aBp['type'] == BX_CMT_BROWSE_TAIL) { + $sPerView = ($aBp['init_view'] != -1 ? 'init' : 'per') . '_view'; + + $aBp['start'] = $aBp['count'] - $aBp[$sPerView]; + if($aBp['start'] < 0) { + $aBp[$sPerView] += $aBp['start']; + $aBp['start'] = 0; + } + } + + $this->_setUserChoice($aDp['type'], $aBp['type'], $aBp['filter']); + } + + protected function _triggerComment() + { + if(!$this->_aSystem['trigger_table'] || !$this->_aSystem['trigger_field_id'] || !$this->_aSystem['trigger_field_comments']) + return false; + + $iId = $this->getId(); + if(!$iId) + return false; + + $iCount = $this->getCommentsCount($iId); + return $this->_oQuery->updateTriggerTable($iId, $iCount); + } + + /** + * Replace provided markers in a string + * @param $mixed string or array to replace markers in + * @return string where all occured markers are replaced + */ + protected function _replaceMarkers ($mixed) + { + return bx_replace_markers($mixed, $this->_aMarkers); + } + + protected function _getUserChoice() + { + $mixedDp = $mixedBpType = $mixedBpFilter = false; + if(!isLogged()) + return array($mixedDp, $mixedBpType, $mixedBpFilter); + + $iUserId = $this->_getAuthorId(); + + $oSession = BxDolSession::getInstance(); + + $mixedDp = $oSession->getValue($this->_sDpSessionKey . $iUserId); + $mixedBpType = $oSession->getValue($this->_sBpSessionKeyType . $iUserId); + $mixedBpFilter = $oSession->getValue($this->_sBpSessionKeyFilter . $iUserId); + + return array($mixedDp, $mixedBpType, $mixedBpFilter); + } + + protected function _setUserChoice($sDp, $sBpType, $sBpFilter) + { + if(!isLogged()) + return; + + $iUserId = $this->_getAuthorId(); + + $oSession = BxDolSession::getInstance(); + + if(!empty($sDp)) + $oSession->setValue($this->_sDpSessionKey . $iUserId, $sDp); + + if(!empty($sBpType)) + $oSession->setValue($this->_sBpSessionKeyType . $iUserId, $sBpType); + + if(!empty($sBpFilter)) + $oSession->setValue($this->_sBpSessionKeyFilter . $iUserId, $sBpFilter); + } + + protected function _sendNotificationEmail($iCmtId, $iCmtParentId) + { + $aCmt = $this->getCommentRow($iCmtId); + $aCmtParent = $this->getCommentRow($iCmtParentId); + if(empty($aCmt) || !is_array($aCmt) || empty($aCmtParent) || !is_array($aCmtParent) || (int)$aCmt['cmt_author_id'] == (int)$aCmtParent['cmt_author_id']) + return; + + $oProfile = $this->_getAuthorObject($aCmtParent['cmt_author_id']); + + if($oProfile instanceof BxDolProfileUndefined) + return; + + $iAccount = $oProfile->getAccountId(); + $aAccount = BxDolAccount::getInstance($iAccount)->getInfo(); + + $aPlus = array(); + $aPlus['sender_display_name'] = $oProfile->getDisplayName(); + $aPlus['reply_text'] = $this->_prepareTextForOutput($aCmt['cmt_text'], $iCmtId); + + $sPageUrl = $this->getBaseUrl(); + if(empty($sPageUrl)) + $sPageUrl = $this->getViewUrl($iCmtParentId); + else + $sPageUrl .= $this->getItemAnchor($iCmtParentId, true); + $aPlus['page_url'] = $sPageUrl; + + $sPageTitle = $this->getObjectTitle(); + if(empty($sPageTitle)) + $sPageTitle = _t('_Content'); + $aPlus['page_title'] = $sPageTitle; + + $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_CommentReplied', $aPlus); + return $aTemplate && sendMail($aAccount['email'], $aTemplate['Subject'], $aTemplate['Body']); + } + + protected function _isShowDoComment($aParams, $isAllowedComment, $bCount) + { + $bShowDoComment = !isset($aParams['show_do_comment']) || $aParams['show_do_comment'] == true; + + return $bShowDoComment && ($isAllowedComment || $bCount); + } + + protected function _isShowCounter($aParams, $isAllowedComment, $bCount) + { + $bShowCounter = isset($aParams['show_counter']) && $aParams['show_counter'] === true; + + return $bShowCounter && ($isAllowedComment || $bCount); + } + + /** + * Note. By default image based controls aren't used. + * Therefore it can be overwritten in custom template. + */ + protected function _getImageDo() + { + return ''; + } + + protected function _getIconDo() + { + return 'far comment'; + } + + protected function _getTitleDo() + { + return '_cmt_txt_do'; + } + + protected function _getParents($aCmt, $iDepthMax, &$iDepth, &$aParents) + { + $iParentId = (int)$aCmt['cmt_parent_id']; + if($iParentId == 0) + return; + + $aParents[] = $iParentId; + $iDepth++; + + if($iDepthMax != 0 && $iDepth == $iDepthMax) + return; + + $aCmt = $this->_oQuery->getCommentsBy(['type' => 'id', 'id' => $iParentId]); + $this->_getParents($aCmt, $iDepthMax, $iDepth, $aParents); + } + + public function _getStructure($mixedItem, $aBp, &$iLevel, &$aStructure) + { + $bItem = !empty($mixedItem) && is_array($mixedItem); + + $iCmtId = $bItem ? (int)$mixedItem['cmt_id'] : 0; + $iCmtIndex = $iCmtId; + + if($bItem) + $aStructure[$iCmtIndex] = [ + 'id' => $mixedItem['cmt_id'], + 'order' => isset($mixedItem['cmt_order']) ? $mixedItem['cmt_order'] : 0, + 'items' => [], + ]; + + if(!$bItem || (int)$mixedItem['cmt_replies'] > 0) { + $aItems = $this->_oQuery->getStructure($this->_iId, $this->_iAuthorId, $iCmtId, $aBp['filter'], $aBp['order']); + if(!empty($aItems)) { + if($bItem && $iLevel < (int)$this->_aSystem['number_of_levels']) { + $iPassLevel = $iLevel + 1; + $aPassStructure = &$aStructure[$iCmtIndex]['items']; + } + else { + $iPassLevel = $iLevel; + $aPassStructure = &$aStructure; //--- Is NEEDED here to correctly check sublevels. + } + + foreach($aItems as $aItem) + $this->_getStructure($aItem, $aBp, $iPassLevel, $aPassStructure); + + //--- Sort subitems + $iWay = isset($aBp['order']['way']) && $aBp['order']['way'] == 'desc' ? -1 : 1; + uasort($aPassStructure, function($aItem1, $aItem2) use ($iWay) { + if($aItem1['order'] == $aItem2['order']) + return 0; + + return $iWay * ($aItem1['order'] < $aItem2['order'] ? -1 : 1); + }); + } + } + } + + public function _getStructureAPI($mixedItem, $aBp, &$iLevel, &$aStructure) + { + $bItem = !empty($mixedItem) && is_array($mixedItem); + + $iCmtId = $bItem ? (int)$mixedItem['cmt_id'] : 0; + $sCmtIndex = 'i' . $iCmtId; + + if($bItem) { + $oMenuActions = BxDolMenu::getObjectInstance($this->_sMenuObjActions); + $oMenuActions->setCmtsData($this, $iCmtId, $aBp); + + $oMenuManage = BxDolMenu::getObjectInstance($this->_sMenuObjManage); + $oMenuManage->setCmtsData($this, $iCmtId, $aBp); + + $aData = $this->getCommentSimple($iCmtId); + $aData = array_merge($this->getDataAPI($aData), [ + 'menu_actions' => $oMenuActions->getCodeAPI(), + 'menu_manage' => $oMenuManage->getCodeAPI(), + ]); + + $aStructure[$sCmtIndex] = [ + 'id' => $iCmtId, + 'order' => isset($mixedItem['cmt_order']) ? $mixedItem['cmt_order'] : 0, + 'data' => $aData, + 'files' => $this->_getAttachments($mixedItem), + 'items' => [], + ]; + } + + if(!$bItem || (int)$mixedItem['cmt_replies'] > 0) { + $aItems = $this->_oQuery->getStructure($this->_iId, $this->_iAuthorId, $iCmtId, $aBp['filter'], $aBp['order']); + if(!empty($aItems)) { + if($bItem && $iLevel < (int)$this->_aSystem['number_of_levels']) { + $iPassLevel = $iLevel + 1; + $aPassStructure = &$aStructure[$sCmtIndex]['items']; + } + else { + $iPassLevel = $iLevel; + $aPassStructure = []; + } + + foreach($aItems as $aItem) + $this->_getStructureAPI($aItem, $aBp, $iPassLevel, $aPassStructure); + + //--- Sort subitems + $iWay = isset($aBp['order']['way']) && $aBp['order']['way'] == 'desc' ? -1 : 1; + + uasort($aPassStructure, function($aItem1, $aItem2) use ($iWay) { + if($aItem1['order'] == $aItem2['order']) + return 0; + + return $iWay * ($aItem1['order'] < $aItem2['order'] ? -1 : 1); + }); + + $aStructure[$sCmtIndex]['items'] = $aPassStructure; + } + } + } + + public function getDataAPI($aData, $aParams = []) + { + $aDataApi = array_merge($aData, [ + 'cmt_url' => '/' . $this->getViewUrl($aData['cmt_id'], false), + 'author_data' => BxDolProfile::getData($aData['cmt_author_id']), + ]); + + $sModule = $this->_aSystem['module']; + $aExtras = [ + 'module' => $sModule, + 'data' => $aData, + 'params' => $aParams, + 'data_api' => &$aDataApi, + ]; + + /** + * @hooks + * @hookdef hook-system-decode_comment_data_api 'system', 'decode_comment_data_api' - hook to override comment data prepared for sending in API response + * - $unit_name - equals `system` + * - $action - equals `decode_comment_data_api` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `module` - [string] module name + * - `data` - [array] comment info array as key&value pairs + * - `params` - [array] params array as key&value pairs + * - `data_api` - [array] by ref, comment data prepared for sending in API response, can be overridden in hook processing + * @hook @ref hook-system-decode_comment_data_api + */ + bx_alert('system', 'decode_comment_data_api', 0, 0, $aExtras); + + /** + * @hooks + * @hookdef hook-bx_dol_comment-decode_comment_data_api '{object_name}', 'decode_comment_data_api' - hook to override comment data prepared for sending in API response + * It's equivalent to @ref hook-system-decode_comment_data_api + * @hook @ref hook-bx_dol_comment-decode_comment_data_api + */ + bx_alert($sModule, 'decode_comment_data_api', 0, 0, $aExtras); + + return $aDataApi; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmtsQuery.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmtsQuery.php new file mode 100644 index 0000000000..033380a4ad --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCmtsQuery.php @@ -0,0 +1,947 @@ +_sTableSystems = BxDolCmts::$sTableSystems; + $this->_sTableIds = BxDolCmts::$sTableIds; + + $this->_oMain = $oMain; + + $this->_sTableFiles = $this->_oMain->getTableNameImages(); + $this->_sTableFiles2Entries = $this->_oMain->getTableNameImages2Entries(); + + $aSystem = $this->_oMain->getSystemInfo(); + $this->_sTable = $aSystem['table']; + $this->_sTriggerTable = $aSystem['trigger_table']; + $this->_sTriggerFieldId = $aSystem['trigger_field_id']; + $this->_sTriggerFieldAuthor = $aSystem['trigger_field_author']; + $this->_sTriggerFieldTitle = $aSystem['trigger_field_title']; + $this->_sTriggerFieldComments = $aSystem['trigger_field_comments']; + + parent::__construct(); + } + + public static function getSystemBy($aParams) + { + $oDb = BxDolDb::getInstance(); + + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query', 1 => []]]; + $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = ""; + + $sSelectClause = "`ts`.*"; + + switch($aParams['type']) { + case 'name': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1]['name'] = $aParams['name']; + + $sWhereClause .= " AND `ts`.`Name` = :name"; + break; + + case 'all': + break; + } + + if(!empty($sOrderClause)) + $sOrderClause = 'ORDER BY ' . $sOrderClause; + + if(!empty($sLimitClause)) + $sLimitClause = 'LIMIT ' . $sLimitClause; + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . BxDolCmts::$sTableSystems . "` AS `ts` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause; + return call_user_func_array(array($oDb, $aMethod['name']), $aMethod['params']); + } + + public static function getInfoBy($aParams) + { + $oDb = BxDolDb::getInstance(); + + $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array())); + $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = ""; + + $sSelectClause = "`ti`.*"; + + switch($aParams['type']) { + case 'uniq_id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1]['uniq_id'] = (int)$aParams['uniq_id']; + + $sWhereClause .= " AND `ti`.`id` = :uniq_id"; + break; + + case 'all': + if(isset($aParams['count']) && $aParams['count'] === true) { + $aMethod['name'] = 'getOne'; + $sSelectClause = "COUNT(`ti`.`id`)"; + } + break; + } + + if(!empty($sOrderClause)) + $sOrderClause = 'ORDER BY ' . $sOrderClause; + + if(!empty($sLimitClause)) + $sLimitClause = 'LIMIT ' . $sLimitClause; + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . BxDolCmts::$sTableIds . "` AS `ti` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause; + return call_user_func_array(array($oDb, $aMethod['name']), $aMethod['params']); + } + + public static function getInfoByUniqId($iUniqId) + { + $oDb = BxDolDb::getInstance(); + + $sQuery = "SELECT + `ti`.`cmt_id` AS `cmt_id`, + `to`.`Name` AS `system_name`, + `to`.`Table` AS `table_name` + FROM `" . BxDolCmts::$sTableIds . "` AS `ti` + INNER JOIN `" . BxDolCmts::$sTableSystems . "` AS `to` ON `ti`.`system_id` = `to`.`ID` + WHERE `ti`.`id` = :uniq_ig + LIMIT 1"; + + $aRow = $oDb->getRow($sQuery, array('uniq_ig' => $iUniqId)); + if(empty($aRow) || !is_array($aRow)) + return $aRow; + + $aRow['cmt_object_id'] = $oDb->getOne("SELECT `cmt_object_id` FROM `" . $aRow['table_name'] . "` WHERE `cmt_id` = :cmt_id LIMIT 1", array( + 'cmt_id' => $aRow['cmt_id'] + )); + + return $aRow; + } + + /** + * @deprecated since version 12.0.1 + */ + public static function getCommentByUniq ($iUnicId) + { + return self::getInfoByUniqId($iUnicId); + } + + public static function getCommentSimpleByUniqId($iUniqId) + { + $oDb = BxDolDb::getInstance(); + + $sQuery = "SELECT + `ti`.`cmt_id` AS `cmt_id`, + `to`.`Table` AS `cmt_table` + FROM `" . BxDolCmts::$sTableIds . "` as `ti` + INNER JOIN `" . BxDolCmts::$sTableSystems . "` as `to` ON `ti`.`system_id` = `to`.`ID` + WHERE `ti`.`id` = :uniq_ig + LIMIT 1"; + + $aData = $oDb->getRow($sQuery, array('uniq_ig' => $iUniqId)); + if(empty($aData) || !is_array($aData)) + return array(); + + return $oDb->getRow("SELECT * FROM `" . $aData['cmt_table'] . "` WHERE `cmt_id` = :cmt_id LIMIT 1", array( + 'cmt_id' => $aData['cmt_id'] + )); + } + + public static function getCommentExtendedByUniqId($iUniqId) + { + $oDb = BxDolDb::getInstance(); + + $sQuery = "SELECT + `ti`.`cmt_id` AS `cmt_id`, + `ti`.`system_id` AS `cmt_system_id`, + `to`.`Table` AS `cmt_table` + FROM `" . BxDolCmts::$sTableIds . "` AS `ti` + INNER JOIN `" . BxDolCmts::$sTableSystems . "` AS `to` ON `ti`.`system_id` = `to`.`ID` + WHERE `ti`.`id` = :uniq_ig + LIMIT 1"; + + $aData = $oDb->getRow($sQuery, array('uniq_ig' => $iUniqId)); + if(empty($aData) || !is_array($aData)) + return array(); + + $sQuery = "SELECT + `tc`.*, + `ti`.`rate`, `ti`.`votes`, + `ti`.`rrate`, `ti`.`rvotes`, + `ti`.`score`, `ti`.`sc_up`, `ti`.`sc_down`, + `ti`.`reports` + FROM `" . $aData['cmt_table'] . "` AS `tc` + LEFT JOIN `" . BxDolCmts::$sTableIds . "` AS `ti` ON `ti`.`system_id` = :cmt_system_id AND `tc`.`cmt_id` = `ti`.`cmt_id` + WHERE `tc`.`cmt_id` = :cmt_id + LIMIT 1"; + + return $oDb->getRow($sQuery, array( + 'cmt_id' => $aData['cmt_id'], + 'cmt_system_id' => $aData['cmt_system_id'] + )); + } + + function getTableName () + { + return $this->_sTable; + } + + /** + * @deprecated since version 10.0.0-B3 and can be removed in later versions. + */ + function setTableNameFiles($sTable) + { + $this->_sTableFiles = $sTable; + } + + /** + * @deprecated since version 10.0.0-B3 and can be removed in later versions. + */ + function setTableNameFiles2Entries($sTable) + { + $this->_sTableFiles2Entries = $sTable; + } + + function getCommentsCountAll ($iId, $iAuthorId = 0, $bForceCalculate = false) + { + $iCount = false; + /** + * @hooks + * @hookdef hook-comment-get_comments_count 'comment', 'get_comments_count' - hook to override number of comments for commented object + * - $unit_name - equals `comment` + * - $action - equals `get_comments_count` + * - $object_id - not used + * - $sender_id - profile id + * - $extra_params - array of additional params with the following array keys: + * - `system` - [string] comment object name + * - `object_id` - [int] commented object id + * - `result` - [int] by ref, number of comments, can be overridden in hook processing + * @hook @ref hook-comment-get_comments_count + */ + bx_alert('comment', 'get_comments_count', 0, $iAuthorId, [ + 'system' => $this->_oMain->getSystemInfo(), + 'object_id' => $iId, + 'result' => &$iCount + ]); + if ($iCount !== false) + return $iCount; + + if ($this->_sTriggerFieldComments && !$bForceCalculate) + return (int)$this->getOne("SELECT `{$this->_sTriggerFieldComments}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = :id", [ + 'id' => $iId + ]); + else + return $this->getCommentsCount($iId, -1, $iAuthorId); + } + + function getCommentsCount ($iId, $iCmtVParentId = -1, $iAuthorId = 0, $sFilter = '') + { + $aBindings = array( + 'cmt_object_id' => $iId, + 'system_id' => $this->_oMain->getSystemId() + ); + + $sWhereClause = $this->getCommentsCheckStatus($iAuthorId); + + if((int)$iCmtVParentId >= 0) { + $aBindings['cmt_vparent_id'] = $iCmtVParentId; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id"; + } + + $sJoinClause = ''; + switch($sFilter) { + case BX_CMT_FILTER_FRIENDS: + case BX_CMT_FILTER_SUBSCRIPTIONS: + $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter)); + + $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId); + $sJoinClause .= ' ' . $aQueryParts['join']; + break; + + case BX_CMT_FILTER_OTHERS: + $aBindings['cmt_author_id'] = $iAuthorId; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` <> :cmt_author_id"; + break; + } + + if(($oCf = $this->_oMain->getObjectContentFilter()) !== false) + $sWhereClause .= $oCf->getSQLParts($this->_sTable, 'cmt_cf'); + + /** + * @hooks + * @hookdef hook-comment-get_comments 'comment', 'get_comments' - hook to override comments list. Is used during comments count retrieving. + * - $unit_name - equals `comment` + * - $action - equals `get_comments` + * - $object_id - not used + * - $sender_id - currently logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `system` - [string] comment object name + * - `join_clause` - [string] by ref, 'join' part of SQL query, can be overridden in hook processing + * - `where_clause` - [string] by ref, 'where' part of SQL query, can be overridden in hook processing + * - `params` - [array] by ref, SQL query bindings array as key&value pairs, can be overridden in hook processing + * @hook @ref hook-comment-get_comments + */ + bx_alert('comment', 'get_comments', 0, bx_get_logged_profile_id(), [ + 'system' => $this->_oMain->getSystemInfo(), + 'join_clause' => &$sJoinClause, + 'where_clause' => &$sWhereClause, + 'params' => &$aBindings + ]); + + $sQuery = "SELECT + COUNT(*) + FROM `{$this->_sTable}` + LEFT JOIN `{$this->_sTableIds}` ON (`{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = :system_id) $sJoinClause + WHERE `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id" . $sWhereClause; + return (int)$this->getOne($sQuery, $aBindings); + } + + function getStructure($iObjectId, $iAuthorId = 0, $iParentId = 0, $sFilter = '', $aOrder = array()) + { + $sSelectClause = $sJoinClause = $sWhereClause = ""; + + $aBindings = array( + 'cmt_object_id' => $iObjectId, + 'system_id' => $this->_oMain->getSystemId() + ); + + if((int)$iParentId >= 0) { + $aBindings['cmt_parent_id'] = $iParentId; + + $sWhereClause .= " AND `tc`.`cmt_parent_id` = :cmt_parent_id"; + } + + if(in_array($sFilter, array(BX_CMT_FILTER_FRIENDS, BX_CMT_FILTER_SUBSCRIPTIONS))) { + $sConnection = $this->_oMain->getConnectionObject($sFilter); + $aQueryParts = BxDolConnection::getObjectInstance($sConnection)->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId); + $sJoinClause .= ' ' . $aQueryParts['join']; + } + + if(isset($aOrder['by']) && isset($aOrder['way'])) + switch($aOrder['by']) { + case BX_CMT_ORDER_BY_DATE: + $sSelectClause .= ", `tc`.`cmt_time` AS `cmt_order`"; + break; + + case BX_CMT_ORDER_BY_POPULAR: + $aOrderFields = array(); + if($this->_oMain->getVoteObject(0) !== false) + $aOrderFields[] = "`ti`.`votes`"; + if($this->_oMain->getScoreObject(0) !== false) + $aOrderFields[] = "`ti`.`score`"; + + if(!empty($aOrderFields)) + $sSelectClause .= ", (" . implode(' + ', $aOrderFields) . ") AS `cmt_order`"; + else + $sSelectClause .= ", `tc`.`id` AS `cmt_order`"; + break; + } + + $sQuery = "SELECT + `tc`.`cmt_id`, + `tc`.`cmt_replies`, + `tc`.`cmt_time` $sSelectClause + FROM `{$this->_sTable}` AS `tc` + LEFT JOIN `{$this->_sTableIds}` AS `ti` ON `tc`.`cmt_id` = `ti`.`cmt_id` AND `ti`.`system_id` = :system_id $sJoinClause + WHERE `tc`.`cmt_object_id` = :cmt_object_id" . $sWhereClause; + return $this->getAll($sQuery, $aBindings); + } + + function getComments ($iId, $iCmtVParentId = 0, $iAuthorId = 0, $sFilter = '', $aOrder = array(), $iStart = 0, $iCount = -1) + { + $aBindings = array( + 'cmt_object_id' => $iId, + 'system_id' => $this->_oMain->getSystemId() + ); + $sFields = $sJoin = ""; + + $sWhereStatus = $this->getCommentsCheckStatus($iAuthorId); + + $sWhereParent = ''; + if((int)$iCmtVParentId >= 0) { + $aBindings['cmt_vparent_id'] = $iCmtVParentId; + + $sWhereParent = " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id"; + } + + $sWhereFilter = ''; + switch($sFilter) { + case BX_CMT_FILTER_PINNED: + $sWhereFilter .= " AND `{$this->_sTable}`.`cmt_pinned` <> 0"; + break; + + case BX_CMT_FILTER_FRIENDS: + case BX_CMT_FILTER_SUBSCRIPTIONS: + $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter)); + + $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId); + $sJoin .= ' ' . $aQueryParts['join']; + break; + } + + $sWhereCf = ''; + if(($oCf = $this->_oMain->getObjectContentFilter()) !== false) + $sWhereCf = $oCf->getSQLParts($this->_sTable, 'cmt_cf'); + + $sOrder = " ORDER BY `{$this->_sTable}`.`cmt_pinned` DESC, `{$this->_sTable}`.`cmt_time` ASC"; + if(isset($aOrder['by']) && isset($aOrder['way'])) { + $aOrder['way'] = strtoupper(in_array($aOrder['way'], array(BX_CMT_ORDER_WAY_ASC, BX_CMT_ORDER_WAY_DESC)) ? $aOrder['way'] : BX_CMT_ORDER_WAY_ASC); + + switch($aOrder['by']) { + case BX_CMT_ORDER_BY_DATE: + $sOrder = " ORDER BY `{$this->_sTable}`.`cmt_time` " . $aOrder['way']; + break; + + case BX_CMT_ORDER_BY_POPULAR: + $aSortFields = array(); + if($this->_oMain->getVoteObject(0) !== false) + array_push($aSortFields, '`' . $this->_sTableIds . '`.`votes`'); + if($this->_oMain->getReactionObject(0) !== false) + array_push($aSortFields, '`' . $this->_sTableIds . '`.`rvotes`'); + if($this->_oMain->getScoreObject(0) !== false) + array_push($aSortFields, '`' . $this->_sTableIds . '`.`score`'); + if(count($aSortFields) == 0) + array_push($aSortFields, '`' . $this->_sTable . '`.`id`'); + + $sOrder = " ORDER BY " . implode($aOrder['way'] . ', ', $aSortFields) . " " . $aOrder['way']; + break; + } + } + + $sLimit = $iCount != -1 ? $this->prepareAsString(" LIMIT ?, ?", (int)$iStart, (int)$iCount) : ''; + + $sWhereClause = $sWhereStatus . $sWhereParent . $sWhereFilter . $sWhereCf; + + $sQuery = "SELECT + `{$this->_sTable}`.*, + `{$this->_sTableIds}`.`id` AS `cmt_unique_id`, + `{$this->_sTableIds}`.`status_admin` AS `cmt_status_admin` + $sFields + FROM `{$this->_sTable}` + LEFT JOIN `{$this->_sTableIds}` ON (`{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = :system_id) + LEFT JOIN `sys_profiles` AS `p` ON `p`.`id` = `{$this->_sTable}`.`cmt_author_id`"; + + /** + * @hooks + * @hookdef hook-comment-get_comments 'comment', 'get_comments' - hook to override comments list. + * - $unit_name - equals `comment` + * - $action - equals `get_comments` + * - $object_id - not used + * - $sender_id - profile id + * - $extra_params - array of additional params with the following array keys: + * - `system` - [string] comment object name + * - `select_clause` - [string] by ref, 'select' part of SQL query, can be overridden in hook processing + * - `join_clause` - [string] by ref, 'join' part of SQL query, can be overridden in hook processing + * - `where_clause` - [string] by ref, 'where' part of SQL query, can be overridden in hook processing + * - `order_clause` - [string] by ref, 'order' part of SQL query, can be overridden in hook processing + * - `limit_clause` - [string] by ref, 'limit' part of SQL query, can be overridden in hook processing + * - `params` - [array] by ref, SQL query bindings array as key&value pairs, can be overridden in hook processing + * @hook @ref hook-comment-get_comments + */ + bx_alert('comment', 'get_comments', 0, $iAuthorId, [ + 'system' => $this->_oMain->getSystemInfo(), + 'select_clause' => &$sQuery, + 'join_clause' => &$sJoin, + 'where_clause' => &$sWhereClause, + 'order_clause' => &$sOrder, + 'limit_clause' => &$sLimit, + 'params' => &$aBindings + ]); + + $sQuery = $sQuery . $sJoin . " WHERE `{$this->_sTable}`.`cmt_object_id`=:cmt_object_id AND (ISNULL(`p`.`status`) OR `p`.`status`='active' OR `{$this->_sTable}`.`cmt_replies`!=0)" . $sWhereClause . $sOrder . $sLimit; + + return $this->getAll($sQuery, $aBindings); + } + + protected function getCommentsCheckStatus($iAuthorId, $sStatus = BX_CMT_STATUS_ACTIVE) + { + if($this->_oMain->isModerator()) + return ''; + + //--- Check viewer as comment author. + $sWhereClause = $this->prepareAsString("`{$this->_sTable}`.`cmt_author_id`=?", $iAuthorId); + + //--- Check viewer as an administrator/moderator of comment author. + $aGroups = []; + $aModules = bx_srv('system', 'get_modules_by_type', ['profile']); + foreach($aModules as $aModule) { + $oModule = BxDolModule::getInstance($aModule['name']); + if(!$oModule || !($oModule instanceof BxBaseModGroupsModule)) + continue; + + $aGroups = array_merge($aGroups, $oModule->getGroupsByFan($iAuthorId, [ + BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR, + BX_BASE_MOD_GROUPS_ROLE_MODERATOR + ])); + } + + if(!empty($aGroups)) + $sWhereClause .= " OR `{$this->_sTable}`.`cmt_author_id` IN (" . $this->implode_escape($aGroups) . ")"; + + return $this->prepareAsString(" AND IF(" . $sWhereClause . ", 1, `{$this->_sTableIds}`.`status_admin`=?) ", $sStatus); + } + + function getCommentsBy($aParams = array()) + { + $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array())); + $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = ""; + + $sSelectClause = "`{$this->_sTable}`.*"; + + if(isset($aParams['object_id'])) { + $aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id"; + } + + switch($aParams['type']) { + case 'id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1]['id'] = (int)$aParams['id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_id` = :id"; + break; + + case 'uniq_id': + $aMethod['name'] = 'getRow'; + $aMethod['params'][1]['system_id'] = $this->_oMain->getSystemId(); + $aMethod['params'][1]['uniq_id'] = (int)$aParams['uniq_id']; + + $sJoinClause = "LEFT JOIN `{$this->_sTableIds}` ON `{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = :system_id"; + $sWhereClause .= " AND `{$this->_sTableIds}`.`id` = :uniq_id"; + break; + + case 'latest': + if(!empty($aParams['object_id'])) { + $aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id"; + } + + if(!empty($aParams['author'])) { + $aMethod['params'][1]['cmt_author_id'] = (int)$aParams['author']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` " . (isset($aParams['others']) && (int)$aParams['others'] == 1 ? "<>" : "=") . " :cmt_author_id"; + } + + $sOrderClause = "`{$this->_sTable}`.`cmt_time` DESC"; + $sLimitClause = ""; + if(isset($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']); + + break; + + case 'parent_id': + $aMethod['params'][1]['cmt_parent_id'] = (int)$aParams['parent_id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_parent_id` = :cmt_parent_id"; + + $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC"; + $sLimitClause = ""; + if(isset($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']); + + break; + + case 'object_id': + $aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id"; + + $sOrderClause = "`{$this->_sTable}`.`cmt_time` " . (!empty($aParams['order_way']) ? strtoupper($aParams['order_way']) : "ASC"); + $sLimitClause = ""; + if(isset($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']); + + break; + + case 'author_id': + $aMethod['params'][1]['cmt_author_id'] = (int)$aParams['author_id']; + + $sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` = :cmt_author_id"; + + $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC"; + $sLimitClause = ""; + if(isset($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']); + + break; + + case 'search_ids': + $aMethod['name'] = 'getColumn'; + + $sSelectClause = "`{$this->_sTable}`.`cmt_id`"; + + if (!empty($aParams['start']) && !empty($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']); + elseif (!empty($aParams['per_page'])) + $sLimitClause = $this->prepareAsString("?", $aParams['per_page']); + + $sWhereConditions = "1"; + foreach($aParams['search_params'] as $sSearchParam => $aSearchParam) { + $sSearchValue = ""; + switch ($aSearchParam['operator']) { + case 'like': + $sSearchValue = " LIKE " . $this->escape("%" . $aSearchParam['value'] . "%"); + break; + + case 'in': + $sSearchValue = " IN (" . $this->implode_escape($aSearchParam['value']) . ")"; + break; + + case 'and': + $iResult = 0; + if (is_array($aSearchParam['value'])) + foreach ($aSearchParam['value'] as $iValue) + $iResult |= pow (2, $iValue - 1); + else + $iResult = (int)$aSearchParam['value']; + + $sSearchValue = " & " . $iResult . ""; + break; + + default: + $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam; + $aMethod['params'][1][$sSearchParam] = $aSearchParam['value']; + } + + $sWhereConditions .= " AND `{$this->_sTable}`.`" . $sSearchParam . "`" . $sSearchValue; + } + + if(($oCf = $this->_oMain->getObjectContentFilter()) !== false) + $sWhereConditions .= $oCf->getSQLParts($this->_sTable, 'cmt_cf'); + + $sWhereClause .= " AND (" . $sWhereConditions . ")"; + + $sOrderClause .= "`{$this->_sTable}`.`cmt_time` ASC"; + break; + + case 'all_ids': + $aMethod['name'] = 'getColumn'; + + $sSelectClause = "`{$this->_sTable}`.`cmt_id`"; + $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC"; + break; + } + + if(!empty($sOrderClause)) + $sOrderClause = 'ORDER BY ' . $sOrderClause; + + if(!empty($sLimitClause)) + $sLimitClause = 'LIMIT ' . $sLimitClause; + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `{$this->_sTable}` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause; + return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']); + } + + function getComment ($iId, $iCmtId) + { + $sFields = $sJoin = ""; + + $oVote = $this->_oMain->getVoteObject($iCmtId); + if($oVote !== false) { + $aSql = $oVote->getSqlParts($this->_sTableIds, 'id'); + + $sFields .= $aSql['fields']; + $sJoin .= $aSql['join']; + } + + $sQuery = $this->prepare("SELECT + `{$this->_sTable}`.*, + `{$this->_sTableIds}`.`id` AS `cmt_unique_id`, + `{$this->_sTableIds}`.`status_admin` AS `cmt_status_admin` + $sFields + FROM `{$this->_sTable}` + LEFT JOIN `{$this->_sTableIds}` ON (`{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = ?) + $sJoin + WHERE `{$this->_sTable}`.`cmt_object_id` = ? AND `{$this->_sTable}`.`cmt_id` = ? + LIMIT 1", $this->_oMain->getSystemId(), $iId, $iCmtId); + return $this->getRow($sQuery); + } + + function getCommentSimple ($iId, $iCmtId) + { + $sQuery = $this->prepare("SELECT * FROM {$this->_sTable} AS `c` WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId); + return $this->getRow($sQuery); + } + + function removeComment ($iId, $iCmtId, $iCmtParentId) + { + $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId); + if (!$this->query($sQuery)) + return false; + + if($iCmtParentId) + $this->updateRepliesCount($iCmtParentId, -1); + + return true; + } + + function saveImages($iSystemId, $iCmtId, $iImageId) + { + $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableFiles2Entries}` SET `system_id`=?, `cmt_id`=?, `image_id`=?", $iSystemId, $iCmtId, $iImageId); + return (int)$this->query($sQuery) > 0; + } + + function getFiles($iSystemId, $iCmtId, $iId = false) + { + $aBindings = array( + 'system_id' => $iSystemId + ); + + $sJoin = ""; + $sWhere = " AND `tf2e`.`system_id` = :system_id "; + + if($iCmtId !== false) { + $aBindings['cmt_id'] = $iCmtId; + + $sWhere .= " AND `tf2e`.`cmt_id` = :cmt_id "; + } + + if($iId !== false) { + $aBindings['cmt_object_id'] = $iId; + + $sWhere .= " AND `te`.`cmt_object_id` = :cmt_object_id"; + $sJoin .= " INNER JOIN `{$this->_sTable}` AS `te` ON (`tf2e`.`cmt_id` = `te`.`cmt_id`) "; + } + + $sQuery = "SELECT + `tf2e`.*, + `tf`.`file_name` AS `file_name`, + `tf`.`mime_type` AS `mime_type`, + `tf`.`size` AS `size`, + `tf`.`dimensions` AS `dimensions` + FROM `{$this->_sTableFiles2Entries}` AS `tf2e` + LEFT JOIN `{$this->_sTableFiles}` AS `tf` ON (`tf2e`.`image_id` = `tf`.`id`) " . $sJoin . " + WHERE 1 " . $sWhere; + + return $this->getAll($sQuery, $aBindings); + } + + public function getFileInfoById($iFileId) + { + $sQuery = "SELECT + `tf2e`.*, + `tf`.`file_name` AS `file_name`, + `tf`.`mime_type` AS `mime_type`, + `tf`.`size` AS `size` + FROM `{$this->_sTableFiles2Entries}` AS `tf2e` + LEFT JOIN `{$this->_sTableFiles}` AS `tf` ON (`tf2e`.`image_id` = `tf`.`id`) + WHERE `tf2e`.`id`=:id "; + + return $this->getRow($sQuery, array( + 'id' => $iFileId + )); + } + + function deleteImages($iSystemId, $iCmtId, $iImageId = false) + { + $sWhereAddon = ""; + $aBindings = array(); + + if ($iSystemId !== false) { + $aBindings['system_id'] = $iSystemId; + + $sWhereAddon .= " AND `system_id` = :system_id "; + } + + if ($iCmtId !== false) { + $aBindings['cmt_id'] = $iCmtId; + + $sWhereAddon .= " AND `cmt_id` = :cmt_id "; + } + + if ($iImageId !== false) { + $aBindings['image_id'] = $iImageId; + + $sWhereAddon .= " AND `image_id` = :image_id "; + } + + return $this->query("DELETE FROM `{$this->_sTableFiles2Entries}` WHERE 1" . $sWhereAddon, $aBindings); + } + + function updateComments($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return; + + return (int)$this->query("UPDATE `{$this->_sTable}` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + function updateRepliesCount($iCmtId, $iCount) + { + $sQuery = $this->prepare("UPDATE `{$this->_sTable}` SET `cmt_replies`=`cmt_replies`+? WHERE `cmt_id`=? LIMIT 1", $iCount, $iCmtId); + return $this->query($sQuery); + } + + function deleteAuthorComments ($iAuthorId, &$aFiles = null, &$aCmtIds = null) + { + $aSystem = $this->_oMain->getSystemInfo(); + + $isDelOccured = 0; + $sQuery = $this->prepare("SELECT `cmt_id`, `cmt_parent_id` FROM {$this->_sTable} WHERE `cmt_author_id` = ? AND `cmt_replies` = 0", $iAuthorId); + $a = $this->getAll ($sQuery); + foreach ($a as $r) { + $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_id` = ?", $r['cmt_id']); + $this->query ($sQuery); + + $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_replies` = `cmt_replies` - 1 WHERE `cmt_id` = ?", $r['cmt_parent_id']); + $this->query ($sQuery); + + $aFilesMore = $this->convertImagesArray($this->getFiles($aSystem['system_id'], $r['cmt_id'])); + $this->deleteImages($aSystem['system_id'], $r['cmt_id']); + if ($aFilesMore && null !== $aFiles) + $aFiles = array_merge($aFiles, $aFilesMore); + + if (null !== $aCmtIds) + $aCmtIds[] = $r['cmt_id']; + + $isDelOccured = 1; + } + $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_author_id` = 0 WHERE `cmt_author_id` = ? AND `cmt_replies` != 0", $iAuthorId); + $this->query ($sQuery); + if ($isDelOccured) + $this->query ("OPTIMIZE TABLE {$this->_sTable}"); + } + + function deleteObjectComments ($iObjectId, &$aFilesReturn = null, &$aCmtIds = null) + { + $aSystem = $this->_oMain->getSystemInfo(); + $aFiles = $this->convertImagesArray($this->getFiles($aSystem['system_id'], false, $iObjectId)); + + if ($aFiles) { + $sQuery = $this->prepare("DELETE FROM {$this->_sTableFiles2Entries} WHERE `system_id` = ? AND `image_id` IN(" . $this->implode_escape($aFiles) . ")", $aSystem['system_id']); + $this->query($sQuery); + } + + if (null !== $aCmtIds) { + $sQuery = $this->prepare("SELECT `cmt_id` FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId); + $aCmtIds = $this->getColumn ($sQuery); + } + + $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId); + $this->query ($sQuery); + $this->query ("OPTIMIZE TABLE {$this->_sTable}"); + + if (null !== $aFilesReturn) + $aFilesReturn = $aFiles; + } + + function deleteAll ($iSystemId, &$aFiles = null, &$aCmtIds = null) + { + // get files + if (null !== $aFiles) + $aFiles = $this->convertImagesArray($this->getFiles($iSystemId, false)); + + // delete files + $this->deleteImages($iSystemId, false); + + if (null !== $aCmtIds) + $aCmtIds = $this->getColumn ("SELECT `cmt_id` FROM {$this->_sTable}"); + + // delete comments + $sQuery = $this->prepare("TRUNCATE TABLE {$this->_sTable}"); + $this->query ($sQuery); + } + + function deleteCmtIds ($iSystemId, $iCmtId) + { + $sQuery = $this->prepare("DELETE FROM {$this->_sTableIds} WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId); + return $this->query ($sQuery); + } + + function getObjectAuthorId($iId) + { + $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldAuthor}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId); + return $this->getOne($sQuery); + } + + function getObjectTitle($iId) + { + $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldTitle}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId); + return $this->getOne($sQuery); + } + + function getObjectPrivacyView($iId, $sField = '') + { + if(empty($sField)) { + $sField = 'allow_view_to'; + if(!$this->isFieldExists($this->_sTriggerTable, $sField)) + return false; + } + + return $this->getOne("SELECT `{$sField}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = :id LIMIT 1", array( + 'id' => $iId + )); + } + + + public function updateTriggerTable($iId, $iCount) + { + if (!$this->_sTriggerFieldComments) + return true; + $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldComments}` = ? WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iCount, $iId); + return $this->query($sQuery); + } + + public function getUniqId($iSystemId, $iCmtId, $aData = []) + { + $sQuery = $this->prepare("SELECT `id` FROM `{$this->_sTableIds}` WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId); + if(($iUniqId = (int)$this->getOne($sQuery)) != 0) + return $iUniqId; + + $aDataDefault = [ + 'system_id' => $iSystemId, + 'cmt_id' => $iCmtId, + 'author_id' => bx_get_logged_profile_id() + ]; + + if(!$this->query("INSERT INTO `{$this->_sTableIds}` SET " . $this->arrayToSQL(array_merge($aDataDefault, $aData)))) + return false; + + return $this->lastId(); + } + + public function updateUniqId($aSetClause, $aWhereClause) + { + if(empty($aSetClause) || empty($aWhereClause)) + return; + + return (int)$this->query("UPDATE `{$this->_sTableIds}` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0; + } + + protected function convertImagesArray($a) + { + if (!$a || !is_array($a)) + return array(); + + $aFiles = array (); + foreach ($a as $aFile) + $aFiles[] = $aFile['image_id']; + return $aFiles; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolConnection.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolConnection.php new file mode 100644 index 0000000000..83e32da210 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolConnection.php @@ -0,0 +1,1088 @@ +isConnected (100, 200, true) ? "100 and 200 are friends" : "100 and 200 aren't friends"; // check if profiles with IDs 100 and 200 have mutual connections + * @endcode + * + * Get mutual content IDs (friends IDs) + * @code + * $oConnectionFriends = BxDolConnection::getObjectInstance('bx_profiles_friends'); // get friends connections object + * if ($oConnectionFriends) // check if connections is available for using + * print_r($oConnection->getConnectedContent(100, 1)); // print array of friends IDs of 100's profile + * @endcode + * + */ +class BxDolConnection extends BxDolFactory implements iBxDolFactoryObject +{ + protected $_sObject; + protected $_aObject; + protected $_oQuery; + protected $_sType; + + /** + * Constructor + * @param $aObject array of connection options + */ + protected function __construct($aObject) + { + parent::__construct(); + + $this->_sObject = $aObject['object']; + $this->_aObject = $aObject; + $this->_aObject['per_page_default'] = 20; + + $this->_sType = $aObject['type']; + + $this->_oQuery = new BxDolConnectionQuery($aObject); + } + + /** + * Get connection object instance by object name + * @param $sObject object name + * @return object instance or false on error + */ + static public function getObjectInstance($sObject) + { + if (!$sObject) + return false; + + if (isset($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject])) + return $GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject]; + + $aObject = BxDolConnectionQuery::getConnectionObject($sObject); + if (!$aObject || !is_array($aObject)) + return false; + + $sClass = empty($aObject['override_class_name']) ? 'BxTemplConnection' : $aObject['override_class_name']; + if (!empty($aObject['override_class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']); + + $o = new $sClass($aObject); + $o->init(); + + return ($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject] = $o); + } + + /** + * Init something here if it's needed. + */ + public function init() + { + } + + /** + * Get connection type. + * return BX_CONNECTIONS_TYPE_ONE_WAY or BX_CONNECTIONS_TYPE_MUTUAL + */ + public function getType() + { + return $this->_sType; + } + + /** + * Get connection table. + * return string with table name. + */ + public function getTable() + { + return $this->_aObject['table']; + } + + /** + * Checks whether connection's Initiator is profile or not. + * return boolean. + */ + public function isProfileInitiator() + { + return (int)$this->_aObject['profile_initiator'] != 0; + } + + /** + * Checks whether connection's Content is profile or not. + * return boolean. + */ + public function isProfileContent() + { + return (int)$this->_aObject['profile_content'] != 0; + } + + /** + * Check whether connection between Initiator and Content can be established. + */ + public function checkAllowedConnect ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true) + { + $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists); + + return $aResult['code'] == 0 ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message']; + } + + public function checkAllowedAddConnection ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true) + { + $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists); + + return $aResult['code'] == 0 || ($aResult['code'] == 4 && $this->_sObject == 'sys_profiles_friends') ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message']; + } + + public function checkAllowedRemoveConnection ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true) + { + $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists); + + return $aResult['code'] == 0 || ($aResult['code'] == 4 && $this->_sObject == 'sys_profiles_friends') ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message']; + } + + /** + * Add new connection. + * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends + * @return array + */ + public function actionAdd ($iContent = 0, $iInitiator = false) + { + if (!$iContent) + $iContent = bx_process_input($_POST['id'], BX_DATA_INT); + + return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'addConnection', '_sys_conn_err_connection_already_exists', true); + } + + /** + * Remove connection. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file. + * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends + * @return array + */ + public function actionRemove ($iContent = 0, $iInitiator = false) + { + if (!$iContent) + $iContent = bx_process_input($_POST['id'], BX_DATA_INT); + + if ($iContent != bx_get_logged_profile_id() && BX_CONNECTIONS_TYPE_MUTUAL == $this->_aObject['type']) { + $a = $this->actionReject($iContent, $iInitiator); + if (false == $a['err']) + return $a; + } + + return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true); + } + + /** + * Reject connection request. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file. + * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends + * @return array + */ + public function actionReject ($iContent = 0, $iInitiator = false) + { + if (!$iContent) + $iContent = bx_process_input($_POST['id'], BX_DATA_INT); + + return $this->_action($iContent, $iInitiator ? $iInitiator : bx_get_logged_profile_id(), 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true); + } + + protected function _action ($iInitiator, $iContent, $sMethod, $sErrorKey, $isMutual = false, $isInvert = false) + { + bx_import('BxDolLanguages'); + + if(!$iContent || !$iInitiator) + return ['err' => true, 'msg' => _t('_sys_conn_err_input_data_is_not_defined')]; + + $sMethodCheck = 'checkAllowed' . bx_gen_method_name($sMethod); + if(($mixedResult = $this->{method_exists($this, $sMethodCheck) ? $sMethodCheck : 'checkAllowedConnect'}($iInitiator, $iContent, false, false, $isInvert)) !== CHECK_ACTION_RESULT_ALLOWED) + return ['err' => true, 'msg' => $mixedResult]; + + if (!$this->$sMethod((int)$iInitiator, (int)$iContent)) { + if ($isMutual && BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType && $this->isConnected((int)$iInitiator, (int)$iContent, false) && !$this->isConnected((int)$iInitiator, (int)$iContent, true)) + return ['err' => true, 'msg' => _t('_sys_conn_err_connection_is_awaiting_confirmation')]; + + return ['err' => true, 'msg' => _t($sErrorKey)]; + } + + return ['err' => false, 'msg' => _t('_sys_conn_msg_success')]; + } + + public function outputActionResult ($mixed, $sFormat = 'json') + { + switch ($sFormat) { + case 'html': + echo $mixed; + break; + + case 'json': + default: + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($mixed); + } + exit; + } + + /** + * Add new connection. + * @param $iInitiator initiator of the connection, in most cases some profile id + * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends + * @return true - if connection was added, false - if connection already exists or error occured + */ + public function addConnection ($iInitiator, $iContent, $aParams = array()) + { + $iMutual = 0; + $iInitiator = (int)$iInitiator; + $iContent = (int)$iContent; + + $aAlertExtras = [ + 'initiator' => &$iInitiator, + 'content' => &$iContent, + 'mutual' => &$iMutual, + 'object' => $this, + ]; + if(!empty($aParams['alert_extras']) && is_array($aParams['alert_extras'])) + $aAlertExtras = array_merge($aAlertExtras, $aParams['alert_extras']); + + /** + * @hooks + * @hookdef hook-bx_dol_connection-connection_before_add '{object_name}', 'connection_before_add' - hook before connection was added. Connection params can be overridden + * - $unit_name - connection object name + * - $action - equals `connection_before_add` + * - $object_id - not used + * - $sender_id - logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `initiator` - [int] by ref, profile id who created the connection, can be overridden in hook processing + * - `content` - [int] by ref, profile id with whom the connection was created, can be overridden in hook processing + * - `mutual` - [int] by ref, if the relation is mutual or not, can be overridden in hook processing + * - `object` - [object] an instance of relation, @see BxDolConnection + * @hook @ref hook-bx_dol_connection-connection_before_add + */ + bx_alert($this->_sObject, 'connection_before_add', 0, bx_get_logged_profile_id(), $aAlertExtras); + + if (!$this->_oQuery->addConnection((int)$iInitiator, (int)$iContent, $iMutual)) + return false; + + /** + * @hooks + * @hookdef hook-bx_dol_connection-connection_added '{object_name}', 'connection_added' - hook after connection was added. Connection params can be overridden + * It's equivalent to @ref hook-bx_dol_connection-connection_before_add + * @hook @ref hook-bx_dol_connection-connection_added + */ + bx_alert($this->_sObject, 'connection_added', 0, bx_get_logged_profile_id(), $aAlertExtras); + + $this->onAdded($iInitiator, $iContent, $iMutual); + + return true; + } + + public function onAdded($iInitiator, $iContent, $iMutual) + { + $this->checkAllowedConnect ($iInitiator, $iContent, true, $iMutual, false); + + $bMutual = false; + if($this->_aObject['type'] == BX_CONNECTIONS_TYPE_ONE_WAY || ($bMutual = ($this->_aObject['type'] == BX_CONNECTIONS_TYPE_MUTUAL && $iMutual))) { + $oProfileQuery = BxDolProfileQuery::getInstance(); + + /** + * Update recommendations. + */ + if($this->_aObject['profile_initiator']) { + $aInitiator = $oProfileQuery->getInfoById($iInitiator); + if(bx_srv($aInitiator['type'], 'act_as_profile')) + BxDolRecommendation::updateData($iInitiator); + } + + if($bMutual && $this->_aObject['profile_content']) { + $aContent = $oProfileQuery->getInfoById($iContent); + if(bx_srv($aContent['type'], 'act_as_profile')) + BxDolRecommendation::updateData($iContent); + } + + /** + * Call socket. + */ + if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()){ + $sMessage = json_encode([ + 'object' => $this->_sObject, + 'action' => 'added' + ]); + + $oSockets->sendEvent('sys_connections', $iInitiator , 'changed', $sMessage); + $oSockets->sendEvent('sys_connections', $iContent , 'changed', $sMessage); + } + } + } + + /** + * Remove connection. + * @param $iInitiator initiator of the connection + * @param $iContent connected content or other profile id in case of friends + * @return true - if connection was removed, false - if connection isn't exist or error occured + */ + public function removeConnection ($iInitiator, $iContent) + { + if (!($aConnection = $this->_oQuery->getConnection((int)$iInitiator, (int)$iContent))) // connection doesn't exist + return false; + + if (!$this->_oQuery->removeConnection((int)$iInitiator, (int)$iContent)) + return false; + + /** + * @hooks + * @hookdef hook-bx_dol_connection-connection_removed '{object_name}', 'connection_removed' - hook after a connection was removed. + * - $unit_name - connection object name + * - $action - equals `connection_removed` + * - $object_id - not used + * - $sender_id - logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `initiator` - [int] profile id who created the connection + * - `content` - [int] profile id with whom the connection was created + * - `mutual` - [int] if the relation is mutual or not + * - `object` - [object] an instance of relation, @see BxDolConnection + * @hook @ref hook-bx_dol_connection-connection_removed + */ + bx_alert($this->_sObject, 'connection_removed', 0, bx_get_logged_profile_id(), array( + 'initiator' => (int)$iInitiator, + 'content' => (int)$iContent, + 'mutual' => isset($aConnection['mutual']) ? $aConnection['mutual'] : 0, + 'object' => $this, + )); + + $this->onRemoved($iInitiator, $iContent); + + return true; + } + + /** + * TODO: Improve - add $iMutual like in onAdded + */ + public function onRemoved($iInitiator, $iContent) + { + /** + * Call socket. + */ + if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()){ + $sMessage = json_encode([ + 'object' => $this->_sObject, + 'action' => 'deleted' + ]); + + $oSockets->sendEvent('sys_connections', $iInitiator , 'changed', $sMessage); + $oSockets->sendEvent('sys_connections', $iContent , 'changed', $sMessage); + } + } + + /** + * Compound function, which calls getCommonContent, getConnectedContent or getConnectedInitiators depending on $sContentType + * @param $sContentType content type to get BX_CONNECTIONS_CONTENT_TYPE_CONTENT, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS or BX_CONNECTIONS_CONTENT_TYPE_COMMON + * @param $iId1 one content or initiator + * @param $iId2 second content or initiator only in case of BX_CONNECTIONS_CONTENT_TYPE_COMMON content type + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getConnectionsAsArray ($sContentType, $iId1, $iId2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType) + return $this->getCommonContent($iId1, $iId2, $isMutual, $iStart, $iLimit, $iOrder); + + if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType) + $sMethod = 'getConnectedInitiators'; + else + $sMethod = 'getConnectedContent'; + + return $this->$sMethod($iId1, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Get common content IDs between two initiators + * @param $iInitiator1 one initiator + * @param $iInitiator2 second initiator + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getCommonContent ($iInitiator1, $iInitiator2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + return $this->_oQuery->getCommonContent($iInitiator1, $iInitiator2, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Get common content count between two initiators + * @param $iInitiator1 one initiator + * @param $iInitiator2 second initiator + * @param $isMutual get mutual connections only + * @return number of connections + */ + public function getCommonContentCount ($iInitiator1, $iInitiator2, $isMutual = false) + { + return $this->_oQuery->getCommonContentCount($iInitiator1, $iInitiator2, $isMutual); + } + + /** + * Get connected content count + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @return number of connections + */ + public function getConnectedContentCount ($iInitiator, $isMutual = false, $iFromDate = 0) + { + return $this->_oQuery->getConnectedContentCount($iInitiator, $isMutual, $iFromDate); + } + + /** + * Get connected content count + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @param $aParams additional params + * @return number of connections + */ + public function getConnectedContentCountExt ($iInitiator, $isMutual = false, $aParams = []) + { + return $this->_oQuery->getConnectedContentCountExt($iInitiator, $isMutual, $aParams); + } + + /** + * Get connected initiators count + * @param $iContent content of the connection + * @param $isMutual get mutual connections only + * @return number of connections + */ + public function getConnectedInitiatorsCount ($iContent, $isMutual = false) + { + return $this->_oQuery->getConnectedInitiatorsCount($iContent, $isMutual); + } + + /** + * Get connected content IDs + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getConnectedContent ($iInitiator, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + return $this->_oQuery->getConnectedContent($iInitiator, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Get connected content IDs for specified type + * @param $iInitiator initiator of the connection + * @param $mixedType type of content or an array of types + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getConnectedContentByType ($iInitiator, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + return $this->_oQuery->getConnectedContentByType($iInitiator, $mixedType, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Get connected initiators IDs + * @param $iContent content of the connection + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getConnectedInitiators ($iContent, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + return $this->_oQuery->getConnectedInitiators($iContent, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Get connected initiators IDs + * @param $iContent content of the connection + * @param $mixedType type of content or an array of types + * @param $isMutual get mutual connections only + * @return array of available connections + */ + public function getConnectedInitiatorsByType ($iContent, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE) + { + return $this->_oQuery->getConnectedInitiatorsByType($iContent, $mixedType, $isMutual, $iStart, $iLimit, $iOrder); + } + + /** + * Similar to getConnectionsAsArray, but for getCommonContentAsSQLParts, getConnectedContentAsSQLParts or getConnectedInitiatorsAsSQLParts methods + * @see getConnectionsAsArray + */ + public function getConnectionsAsSQLParts ($sContentType, $sContentTable, $sContentField, $iId1, $iId2, $isMutual = false) + { + if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType) + return $this->getCommonContentAsSQLParts($sContentTable, $sContentField, $iId1, $iId2, $isMutual); + + if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType) + $sMethod = 'getConnectedInitiatorsAsSQLParts'; + else + $sMethod = 'getConnectedContentAsSQLParts'; + + return $this->$sMethod($sContentTable, $sContentField, $iId1, $isMutual); + } + + /** + * Get necessary parts of SQL query to use connections in other queries + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @return array of SQL string parts, for now 'join' part only is returned + */ + public function getCommonContentAsSQLParts ($sContentTable, $sContentField, $iInitiator1, $iInitiator2, $isMutual = false) + { + return $this->_oQuery->getCommonContentSQLParts($sContentTable, $sContentField, $iInitiator1, $iInitiator2, $isMutual); + } + + /** + * Get necessary parts of SQL query to use connections in other queries + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @return array of SQL string parts, for now 'join' part only is returned + */ + public function getConnectedContentAsSQLParts ($sContentTable, $sContentField, $iInitiator, $isMutual = false) + { + return $this->_oQuery->getConnectedContentSQLParts($sContentTable, $sContentField, $iInitiator, $isMutual); + } + + public function getConnectedContentAsSQLPartsExt ($sContentTable, $sContentField, $iInitiator, $isMutual = false) + { + return $this->_oQuery->getConnectedContentSQLPartsExt($sContentTable, $sContentField, $iInitiator, $isMutual); + } + + /** + * Get necessary parts of SQL query to use connections in other queries + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $sInitiatorTable initiator table or alias + * @param $sInitiatorField initiator table field or field alias + * @param $iInitiator initiator of the connection + * @param $isMutual get mutual connections only + * @return array of SQL string parts, for now 'join' part only is returned + */ + public function getConnectedContentAsSQLPartsMultiple ($sContentTable, $sContentField, $sInitiatorTable, $sInitiatorField, $isMutual = false) + { + return $this->_oQuery->getConnectedContentSQLPartsMultiple($sContentTable, $sContentField, $sInitiatorTable, $sInitiatorField, $isMutual); + } + + /** + * Get necessary parts of SQL query to use connections in other queries + * @param $sInitiatorTable initiator table or alias + * @param $sInitiatorField initiator table field or field alias + * @param $iContent content of the connection + * @param $isMutual get mutual connections only + * @return array of SQL string parts, for now 'join' part only is returned + */ + public function getConnectedInitiatorsAsSQLParts ($sInitiatorTable, $sInitiatorField, $iContent, $isMutual = false) + { + return $this->_oQuery->getConnectedInitiatorsSQLParts($sInitiatorTable, $sInitiatorField, $iContent, $isMutual); + } + + /** + * Get necessary parts of SQL query to use connections in other queries + * @param $sInitiatorTable initiator table or alias + * @param $sInitiatorField initiator table field or field alias + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $isMutual get mutual connections only + * @return array of SQL string parts, for now 'join' part only is returned + */ + public function getConnectedInitiatorsAsSQLPartsMultiple ($sInitiatorTable, $sInitiatorField, $sContentTable, $sContentField, $isMutual = false) + { + return $this->_oQuery->getConnectedInitiatorsSQLPartsMultiple ($sInitiatorTable, $sInitiatorField, $sContentTable, $sContentField, $isMutual); + } + + /** + * Similar to getConnectionsAsArray, but for getCommonContentAsCondition, getConnectedContentAsCondition or getConnectedInitiatorsAsCondition methods + * @see getConnectionsAsArray + */ + public function getConnectionsAsCondition ($sContentType, $sContentField, $iId1, $iId2, $isMutual = false) + { + if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType) + return $this->getCommonContentAsCondition($sContentField, $iId1, $iId2, $isMutual); + + if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType) + $sMethod = 'getConnectedInitiatorsAsCondition'; + else + $sMethod = 'getConnectedContentAsCondition'; + + return $this->$sMethod($sContentField, $iId1, $isMutual); + } + + /** + * Get necessary condition array to use connections in search classes + * @param $sContentField content table field name + * @param $iInitiator initiator of the connection + * @param $iMutual get mutual connections only + * @return array of conditions, for now with 'restriction' and 'join' parts + */ + public function getCommonContentAsCondition ($sContentField, $iInitiator1, $iInitiator2, $iMutual = false) + { + return array( + + 'restriction' => array ( + 'connections_' . $this->_sObject => array( + 'value' => $iInitiator1, + 'field' => 'initiator', + 'operator' => '=', + 'table' => 'c', + ), + 'connections_mutual_' . $this->_sObject => array( + 'value' => $iMutual, + 'field' => 'mutual', + 'operator' => '=', + 'table' => 'c', + ), + 'connections2_' . $this->_sObject => array( + 'value' => $iInitiator2, + 'field' => 'initiator', + 'operator' => '=', + 'table' => 'c2', + ), + 'connections2_mutual_' . $this->_sObject => array( + 'value' => $iMutual, + 'field' => 'mutual', + 'operator' => '=', + 'table' => 'c2', + ), + ), + + 'join' => array ( + 'connections_' . $this->_sObject => array( + 'type' => 'INNER', + 'table' => $this->_aObject['table'], + 'table_alias' => 'c', + 'mainField' => $sContentField, + 'onField' => 'content', + 'joinFields' => array(), + ), + 'connections2_' . $this->_sObject => array( + 'type' => 'INNER', + 'table' => $this->_aObject['table'], + 'table_alias' => 'c2', + 'mainTable' => 'c', + 'mainField' => 'content', + 'onField' => 'content', + 'joinFields' => array(), + ), + ), + + ); + } + + /** + * Get necessary condition array to use connections in search classes + * @param $sContentField content table field name + * @param $iInitiator initiator of the connection + * @param $iMutual get mutual connections only + * @return array of conditions, for now with 'restriction' and 'join' parts + */ + public function getConnectedContentAsCondition ($sContentField, $iInitiator, $iMutual = false) + { + $sOperation = '='; + if(is_array($iInitiator)) + $sOperation = 'in'; + + return array( + + 'restriction' => array ( + 'connections_' . $this->_sObject => array( + 'value' => $iInitiator, + 'field' => 'initiator', + 'operator' => $sOperation, + 'table' => $this->_aObject['table'], + ), + 'connections_mutual_' . $this->_sObject => array( + 'value' => $iMutual, + 'field' => 'mutual', + 'operator' => '=', + 'table' => $this->_aObject['table'], + ), + ), + + 'join' => array ( + 'connections_' . $this->_sObject => array( + 'type' => 'INNER', + 'table' => $this->_aObject['table'], + 'mainField' => $sContentField, + 'onField' => 'content', + 'joinFields' => array(),//'initiator'), + ), + ), + + ); + } + + /** + * Get necessary condition array to use connections in search classes + * @param $sContentField content table field name + * @param $iInitiator initiator of the connection + * @param $iMutual get mutual connections only + * @return array of conditions, for now with 'restriction' and 'join' parts + */ + public function getConnectedInitiatorsAsCondition ($sContentField, $iContent, $iMutual = false) + { + $sOperation = '='; + if(is_array($iContent)) + $sOperation = 'in'; + + return array( + + 'restriction' => array ( + 'connections_' . $this->_sObject => array( + 'value' => $iContent, + 'field' => 'content', + 'operator' => $sOperation, + 'table' => $this->_aObject['table'], + ), + 'connections_mutual_' . $this->_sObject => array( + 'value' => $iMutual, + 'field' => 'mutual', + 'operator' => '=', + 'table' => $this->_aObject['table'], + ), + ), + + 'join' => array ( + 'connections_' . $this->_sObject => array( + 'type' => 'INNER', + 'table' => $this->_aObject['table'], + 'mainField' => $sContentField, + 'onField' => 'initiator', + 'joinFields' => array(),//'initiator'), + ), + ), + + ); + } + + /** + * Check if initiator and content are connected. + * In case if friends this function in conjunction with isMutual parameter can be used to check pending friend requests. + * @param $iInitiator initiator of the connection + * @param $iContent connected content or other profile id in case of friends + * @return true - if content and initiator are connected or false - in all other cases + */ + public function isConnected ($iInitiator, $iContent, $isMutual = false) + { + $oConnection = $this->_oQuery->getConnection ($iInitiator, $iContent); + if (!$oConnection) + return false; + return false === $isMutual ? true : (isset($oConnection['mutual']) ? $oConnection['mutual'] : false); + } + + /** + * Check if initiator and content are connected but connetion is not mutual, for checking pending connection requests. + * This method makes sense only when type of connection is mutual. + * @param $iInitiator initiator of the connection + * @param $iContent connected content or other profile id in case of friends + * @return true - if content and initiator are connected but connection is not mutual or false in all other cases + */ + public function isConnectedNotMutual ($iInitiator, $iContent) + { + $oConnection = $this->_oQuery->getConnection ($iInitiator, $iContent); + if (!$oConnection) + return false; + return $oConnection['mutual'] ? false : true; + } + + public function getConnection ($iInitiator, $iContent) + { + return $this->_oQuery->getConnection($iInitiator, $iContent); + } + + public function getConnectionById ($iId) + { + return $this->_oQuery->getConnectionById($iId); + } + + /** + * Must be called when some content is deleted which can have connections as 'content' or as 'initiator', to delete any associated data + * @param $iId which can be as conetnt ot initiator + * @return true if some connections were deleted + */ + public function onDeleteInitiatorAndContent ($iId) + { + $b = $this->onDeleteInitiator ($iId); + $b = $this->onDeleteContent ($iId) || $b; + return $b; + } + + /** + * Must be called when some content is deleted which can have connections as 'initiator', to delete any associated data + * @param $iIdInitiator initiator id + * @return true if some connections were deleted + */ + public function onDeleteInitiator ($iIdInitiator) + { + if(!$this->_oQuery->onDelete ($iIdInitiator, 'initiator')) + return false; + + /** + * @hooks + * @hookdef hook-bx_dol_connection-connection_removed_all '{object_name}', 'connection_removed_all' - hook after all connections with deleted 'initiator' were removed. + * - $unit_name - connection object name + * - $action - equals `connection_removed_all` + * - $object_id - not used + * - $sender_id - logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `initiator` - [int] profile id who created the connection + * - `object` - [object] an instance of relation, @see BxDolConnection + * @hook @ref hook-bx_dol_connection-connection_removed_all + */ + bx_alert($this->_sObject, 'connection_removed_all', 0, bx_get_logged_profile_id(), array( + 'initiator' => (int)$iIdInitiator, + 'object' => $this, + )); + + return true; + } + + /** + * Must be called when some content is deleted which can have connections as 'content', to delete any associated data + * @param $iIdInitiator initiator id + * @return true if some connections were deleted + */ + public function onDeleteContent ($iIdContent) + { + if(!$this->_oQuery->onDelete ($iIdContent, 'content')) + return false; + + /** + * @hooks + * @hookdef hook-bx_dol_connection-connection_removed_all '{object_name}', 'connection_removed_all' - hook after all connections with deleted 'content' were removed. + * - $unit_name - connection object name + * - $action - equals `connection_removed_all` + * - $object_id - not used + * - $sender_id - logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `content` - [int] profile id with whom the connection was created + * - `object` - [object] an instance of relation, @see BxDolConnection + * @hook @ref hook-bx_dol_connection-connection_removed_all + */ + bx_alert($this->_sObject, 'connection_removed_all', 0, bx_get_logged_profile_id(), array( + 'content' => (int)$iIdContent, + 'object' => $this, + )); + + return true; + } + + + /** + * Must be called when module (which can have connections as 'content' or as 'initiator') is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sTable table name with data which have assiciations with the connection + * @param $sFieldId id field name which is associated with the connection + * @return number of deleted connections + */ + public function onModuleDeleteInitiatorAndContent ($sTable, $sFieldId) + { + $iAffected = $this->onModuleDeleteInitiator ($sTable, $sFieldId); + $iAffected += $this->onModuleDeleteContent ($sTable, $sFieldId); + return $iAffected; + } + + /** + * Must be called when module (which can have connections as 'initiator') is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sTable table name with data which have assiciations with the connection + * @param $sFieldId id field name which is associated with the connection + * @return number of deleted connections + */ + public function onModuleDeleteInitiator ($sTable, $sFieldId) + { + return $this->_oQuery->onModuleDelete ($sTable, $sFieldId, 'initiator'); + } + + /** + * Must be called when module (which can have connections as 'content') is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sTable table name with data which have assiciations with the connection + * @param $sFieldId id field name which is associated with the connection + * @return number of deleted connections + */ + public function onModuleDeleteContent ($sTable, $sFieldId) + { + return $this->_oQuery->onModuleDelete ($sTable, $sFieldId, 'content'); + } + + + /** + * Must be called when module (which can have connections as 'content' or as 'initiator' with 'sys_profiles' table) is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sModuleName module name to delete connections for + * @return number of deleted connections + */ + public function onModuleProfileDeleteInitiatorAndContent ($sModuleName) + { + $iAffected = $this->onModuleProfileDeleteInitiator ($sModuleName); + $iAffected += $this->onModuleProfileDeleteContent ($sModuleName); + return $iAffected; + } + + /** + * Must be called when module (which can have connections as 'initiator' with 'sys_profiles' table) is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sModuleName module name to delete connections for + * @return number of deleted connections + */ + public function onModuleProfileDeleteInitiator ($sModuleName) + { + return $this->_oQuery->onModuleProfileDelete ($sModuleName, 'initiator'); + } + + /** + * Must be called when module (which can have connections as 'content' with 'sys_profiles' table) is deleted, to delete any associated data. + * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property. + * @param $sModuleName module name to delete connections for + * @return number of deleted connections + */ + public function onModuleProfileDeleteContent ($sModuleName) + { + return $this->_oQuery->onModuleProfileDelete ($sModuleName, 'content'); + } + + protected function _checkAllowedConnect ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true) + { + $sErr = _t('_sys_txt_access_denied'); + + if(!$iInitiator || !$iContent || $iInitiator == $iContent) + return ['code' => 1, 'message' => $sErr]; + + $oInitiator = BxDolProfile::getInstance($iInitiator); + $oContent = BxDolProfile::getInstance($iContent); + if(!$oInitiator || !$oContent) + return ['code' => 2, 'message' => $sErr]; + + // check ACL + if(($mixedResult = $this->_checkAllowedConnectInitiator($oInitiator, $isPerformAction)) !== CHECK_ACTION_RESULT_ALLOWED) + return ['code' => 3, 'message' => $mixedResult]; + + $iCode = 0; + $sMessage = ''; + + // check content's visibility + if(!$this->isConnected($iContent, $iInitiator) && ($mixedResult = $this->_checkAllowedConnectContent($oContent)) !== CHECK_ACTION_RESULT_ALLOWED) + list($iCode, $sMessage) = [4, $mixedResult]; + + if(!$isCheckExists) + return ['code' => $iCode, 'message' => $sMessage != '' ? $sMessage : null]; + + if($isSwap) + $isConnected = $this->isConnected($iContent, $iInitiator, $isMutual); + else + $isConnected = $this->isConnected($iInitiator, $iContent, $isMutual); + + if($isInvertResult) + $isConnected = !$isConnected; + + if($isConnected) + list($iCode, $sMessage) = [5, $sErr]; + + return ['code' => $iCode, 'message' => $sMessage != '' ? $sMessage : null]; + } + + protected function _checkAllowedConnectInitiator ($oInitiator, $isPerformAction = false) + { + $aCheck = checkActionModule($oInitiator->id(), 'connect', 'system', $isPerformAction); + if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED) + return $aCheck[CHECK_ACTION_MESSAGE]; + + return CHECK_ACTION_RESULT_ALLOWED; + } + + protected function _checkAllowedConnectContent ($oContent) + { + return $oContent->checkAllowedProfileView(); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronAccount.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronAccount.php new file mode 100644 index 0000000000..be651a20ff --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronAccount.php @@ -0,0 +1,172 @@ +_aParseParams = array( + 'site_title' => getParam('site_title') + ); + } + + public function processing() + { + set_time_limit(0); + ignore_user_abort(); + + $aEmails = []; + + /* password expired soon email */ + bx_import('BxTemplAcl'); + + $oAclDb = BxDolAclQuery::getInstance(); + $oAccountDb = BxDolAccountQuery::getInstance(); + + $aMemberships = []; + $oAclDb->getLevels(['type' => 'password_can_expired'], $aMemberships, false); + + foreach($aMemberships as $aMembership) { + $aProfiles = $oAclDb->getProfilesByMembership([$aMembership['id']]); + foreach($aProfiles as $aProfile) { + $oAccount = BxDolAccount::getInstance($aProfile['account_id']); + if(!$oAccount) + continue; + + $iPasswordExpired = $oAccount->getPasswordExpiredDate($aMembership['password_expired']); + $aAccountInfo = $oAccountDb->getInfoById($aProfile['account_id']); + $iLastPassChanged = $oAccountDb->getLastPasswordChanged($aProfile['account_id']); + if ( + !in_array($aAccountInfo['email'], $aEmails) + && ($aMembership['password_expired'] - $aMembership['password_expired_notify']) * 86400 + $iLastPassChanged < time() + && $iPasswordExpired >= time() + ){ + $aPlus = array(); + $aPlus['expired_date'] = date('d.m.Y', $iPasswordExpired); + $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_AccountPasswordExpired', $aPlus); + + sendMail($aAccountInfo['email'], $aTemplate['Subject'], $aTemplate['Body'], $aProfile['id']); + $aEmails[] = $aAccountInfo['email']; + } + + $oAccountDb->updatePasswordExpired($aProfile['account_id'], $iPasswordExpired); + } + } + + /* new accounts email */ + if(getParam('enable_notification_account') != 'on') + return; + + $this->processNewlyJoined(); + + if(empty($this->_aParseParams['account_count']) || empty($this->_aParseParams['account_output'])) + return; + + $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Account', $this->_aParseParams); + if(empty($aTemplate)) + return; + + $aSent = array(); + $aProfiles = BxDolAclQuery::getInstance()->getProfilesByMembership(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR)); + foreach($aProfiles as $aProfile) { + $oProfile = BxDolProfile::getInstance($aProfile['id']); + if(!$oProfile) + continue; + + $oAccount = $oProfile->getAccountObject(); + if(!$oAccount || !$oAccount->isConfirmed()) + continue; + + $aAccount = $oAccount->getInfo(); + if((int)$aAccount['receive_news'] != 1) + continue; + + if(in_array($aAccount['email'], $aSent)) + continue; + + if(sendMail($aAccount['email'], $aTemplate['Subject'], $aTemplate['Body'], $aProfile['id'])) + $aSent[] = $aAccount['email']; + } + + + } + + protected function processNewlyJoined() + { + $aAccounts = BxDolAccountQuery::getInstance()->getAccounts(array('type' => 'by_join_date', 'date' => time() - 86400)); + if(empty($aAccounts) || !is_array($aAccounts)) + return; + + $iAccounts = 0; + $aTmplVarsItems = array(); + foreach($aAccounts as $aAccount) { + $oProfile = BxDolProfile::getInstance($aAccount['profile_id']); + if(!$oProfile) + continue; + + $iId = $oProfile->id(); + $sUrl = $oProfile->getUrl(); + $bUrl = $oProfile->getModule() != 'system'; + + $sTitle = $oProfile->getDisplayName(); + $sTitleAttr = bx_html_attribute($sTitle); + + $sThumbUrl = $oProfile->getThumb(); + $bThumbUrl = $oProfile->hasImage(); + + $aTmplVarsItems[] = array( + 'bx_if:show_thumb_image' => array( + 'condition' => $bThumbUrl, + 'content' => array( + 'thumb_url' => $sThumbUrl + ) + ), + 'bx_if:show_thumb_letter' => array( + 'condition' => !$bThumbUrl, + 'content' => array( + 'color' => implode(', ', BxDolTemplate::getColorCode($iId, 1.0)), + 'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1)) + ) + ), + 'bx_if:show_title_link' => array( + 'condition' => $bUrl, + 'content' => array( + 'content_url' => $sUrl, + 'content_title' => $sTitle, + 'content_title_attr' => $sTitleAttr + ) + ), + 'bx_if:show_title_text' => array( + 'condition' => !$bUrl, + 'content' => array( + 'content_title' => $sTitle, + 'content_title_attr' => $sTitleAttr + ) + ), + 'email' => $aAccount['email'] + ); + + $iAccounts += 1; + } + + if(empty($aTmplVarsItems)) + return; + + $this->_aParseParams['account_count'] = $iAccounts; + $this->_aParseParams['account_output'] = BxDolTemplate::getInstance()->parseHtmlByName('et_account.html', array( + 'bx_repeat:items' => $aTmplVarsItems + )); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronPruning.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronPruning.php new file mode 100644 index 0000000000..56de71f260 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolCronPruning.php @@ -0,0 +1,153 @@ +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 view tracks + $iViewTracks = BxDolView::pruning(); + + // clean vote tracks + $iVoteTracks = BxDolVote::pruning(); + + // clean score tracks + $iScoreTracks = BxDolScore::pruning(); + + // clean favorite tracks + $iFavoriteTracks = BxDolFavorite::pruning(); + + // clean report tracks + $iReportTracks = BxDolReport::pruning(); + + // clean accounts without profiles + $iDeletedAccounts = BxDolAccount::pruning(); + + // clean expired keys + $oKey = BxDolKey::getInstance(); + $iDeletedKeys = $oKey ? $oKey->prune() : 0; + + // clean ai related data + $iDeletedItems = BxDolAI::pruning(); + + echo call_user_func_array('_t', ['_sys_pruning_db', + $iDeleteMemLevels, + $iSessions, $iDeletedKeys, + $iDeletedExpiredTokens, $iDeletedTranscodedImages, + $iDeletedAccounts, + $iViewTracks, $iVoteTracks, $iScoreTracks, $iFavoriteTracks, $iReportTracks + ]); + } + + /** + * Clean tmp folders (tmp, cache) by deleting old files (by default older than 1 month) + */ + protected function cleanTmpFolders() + { + $aDirsToClean = array( + array('dir' => BX_DIRECTORY_PATH_TMP, 'prefix' => '', 'file_life_time' => 2592000), + array('dir' => BX_DIRECTORY_PATH_CACHE, 'prefix' => '', 'file_life_time' => 2592000), + array('dir' => BX_DIRECTORY_PATH_CACHE_PUBLIC, 'prefix' => '', 'file_life_time' => 3600), + array('dir' => BX_DIRECTORY_PATH_CACHE_PUBLIC, 'prefix' => parse_url(BX_DOL_URL_ROOT, PHP_URL_HOST) . '_', 'file_life_time' => 86400), + ); + + $iNumTmp = 0; + $iNumDel = 0; + + foreach ($aDirsToClean as $a) { + + $sDir = $a['dir']; + $iTmpFileLife = $a['file_life_time']; + $sPrefix = $a['prefix']; + $sPrefixLen = strlen($a['prefix']); + + if (!($h = opendir($sDir))) + continue; + + while ($sFile = readdir($h)) { + + if ('.' == $sFile || '..' == $sFile || '.' == $sFile[0]) + continue; + + if ($sPrefix && 0 !== strncmp($sFile, $sPrefix, $sPrefixLen)) + 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(); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolGrid.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolGrid.php new file mode 100644 index 0000000000..58a2946eef --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolGrid.php @@ -0,0 +1,665 @@ + 2^(1-1) = 1 + * - user level id = 2 -> 2^(2-1) = 2 + * - user level id = 3 -> 2^(3-1) = 4 + * - user level id = 4 -> 2^(4-1) = 8 + * - override_class_name: user defined class name which is derived from BxTemplGrid. + * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders. + * + * 2. Specify field names (columns in the grid) in sys_grid_fields table: + * + * - object: name of the Grid object. + * - name: name of the field, it must refer to the SQL field name in the case of 'Sql' 'source_type' or index of the 2 dimentional array in the case of 'Array' 'source_type'. + * - title: title of the field, the language key. + * - width: width of the column in % or px, pt, etc. + * - translatable: if field contains language key and it is needed to display translation for this key - set it to 1, by default 0. + * - params: searialized array of additional params: + * - display: display function from BxDolFormCheckerHelper class, for example to convert unix timestamp to the regular date/time string. + * - attr_cell: tag attributes for the data cell. + * - attr_head: tag attributes for the header cell. + * - order: order of the field. + * + * There are some fields which are always available, additionally to the provided set of fields: + * + * - order: display column as dragable handle, it makes sense if you have data ordered by some field + * and it is specified in field_order, field_id and table fields; reordering is not correctly + * working with paginate, so make sure that paginate_per_page number is big enough to show all records; + * reordering is working with Sql source_type. + * - checkbox: display column with checkboxes, so several records can be selected for bulk action; + * you need to specify 'field_id' field, so every checkbox have unique row id; + * you need to specify bulk actions separately in 'sys_grid_actions' table; + * you can override '_isCheckboxSelected' function to display checkbox as checked by default. + * - actions: display column with single actions, displayed as buttons; you need to specify field_id field, + * so every action is provided with unique row id; you need to specify single actions separately in sys_grid_actions table. + * + * 3. Add actions to sys_grid_actions table: + * + * - object: name of the Grid object. + * - type: action type, one of the following: + * - bulk: bulk action, to perform on the set of records, the action is usually displaed below the grid. + * - single: simple action, to perform on one record, the action is usually displayed in the grid row. + * - independent: independent actionm which is not related to any rowm the action is usually displayed above the grid. + * - name: action name. + * - title: title of the action, the language key. + * - icon: display action as icon, title need to be empty in this case. + * - confirm: ask confirmation before performing the action, 0 or 1. + * - order: order of the action in particular actions set by type. + * + * Usually you need to handle actions manually, but there are several actions which are available by default: + * + * - delete: delete the record, it works automatically when 'source_type' is 'Sql' and 'field_id', 'table' fields are specified. + * + * + * + * @section grid_display_custom_cell Displaying custom cell + * + * Cell is displayed with default design. It is possible to easily customize its design by specifying custom attributes as 'attr_cell' in params field in sys_grid_fields table. + * + * If it is not enough, you can customize it even more by adding the method to your custom class with the following format: + * _getCell[field name] + * where [field name] is the name of the field you want to have custom look with the capital first letter. + * + * For example: + * + * @code + * protected function _getCellStatus ($mixedValue, $sKey, $aField, $aRow) { + * + * $sAttr = $this->_convertAttrs( + * $aField, 'attr_cell', + * false, + * isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles + * ); + * return '' . $mixedValue . ''; + * } + * @endcode + * + * Above example is displaying user's status using different colors depending on the status value. Please note that you need to convert attributes by adding some default classes or styles if you need. + * + * + * + * @section grid_display_custom_header Displaying custom column header + * + * This is working similar to displaying custom cell. It easily customize its design by specifying custom attributes as 'attr_head' in params field in sys_grid_fields table. + * If it is not enough, you can customize it even more by adding the method to your custom class with the following format: + * _getCellHeader[field name] + * where [field name] is the name of the field you want to have custom look with the capital first letter. + * + * For example: + * + * @code + * protected function _getCellHeaderStatus ($sKey, $aField) { + * $s = parent::_getCellHeaderDefault($sKey, $aField); + * return preg_replace ('/(.*?)<\/th>/', '', $s); + * } + * @endcode + * + * The above example replaces column header text with the image. + * + * + * + * @section grid_display_custom_action Displaying custom action + * + * All actions are displayed as buttons. Bulk and independent actions are displaed as big buttons and single actions are displayed as small buttons. + * + * It is possible to completely customize it by adding the following method to your custom class: + * _getAction[action name] + * where [action name] is the action name with the capital first letter. + * + * For example: + * + * @code + * protected function _getActionCustom1 ($sType, $sKey, $a, $isSmall = false) { + * $sAttr = $this->_convertAttrs( + * $a, 'attr', + * 'bx-btn bx-def-margin-sec-left' . ($isSmall ? ' bx-btn-small' : '') // add default classes + * ); + * return ''; + * } + * @endcode + * + * The above example disables default onclick event and just displays an alert. Please note that you need to convert attributes by adding some default classes or styles if you need. + * + * + * + * @section grid_add_action_handler Add action handler + * + * As it was mentioned earlier only several actions can be handled automatically, all other actions must be processed manually. + * To add action handler you need to add method to your custom class with the following format: + * performAction[action name] + * where [action name] is the action name with the capital first letter. + * + * For example: + * + * @code + * public function performActionApprove() { + * + * $iAffected = 0; + * $aIds = bx_get('ids'); + * if (!$aIds || !is_array($aIds)) { + * echoJson(array()); + * exit; + * } + * + * $aIdsAffected = array (); + * foreach ($aIds as $mixedId) { + * if (!$this->_approve($mixedId)) + * continue; + * $aIdsAffected[] = (int)$mixedId; + * $iAffected++; + * } + * + * echoJson(array( + * 'msg' => $iAffected > 0 ? sprintf("%d profiles successfully activated", $iAffected) : "Profile(s) activation failed", + * 'grid' => $this->getCode(false), + * 'blink' => $aIdsAffected, + * )); + * } + * + * protected function _approve ($mixedId) { + * $oDb = BxDolDb::getInstance(); + * $sTable = $this->_aOptions['table']; + * $sFieldId = $this->_aOptions['field_id']; + * $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `Status` = 'Active' WHERE `{$sFieldId}` = ?", $mixedId); + * return $oDb->query($sQuery); + * } + * @endcode + * + * The action can be used as 'single' or 'bulk', in the case of 'single' action 'ids' array always has one element. + * + * As the result, action must outputs JSON array, which is done by echoJson function. + * The defined indexes in the array determines behavior after action is performed, the following behaviors are supported: + * + * - msg: display javascript alert message. + * - grid: reload grid data with the provided HTML code. + * - popup: display popup with the provided HTML code. + * - blink: highlight(blink effect) the specified rows, by the ids. + * + */ + +class BxDolGrid extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable +{ + protected $_bIsApi; + + protected $_aMarkers = array (); + + protected $_sObject; + protected $_aOptions; + + protected $_aBrowseParams; + protected $_sDefaultSortingOrder = 'ASC'; + protected $_iTotalCount = 0; + + protected $_bActionCsrfChecking; + + /** + * Constructor + * @param $aOptions array of grid options + */ + protected function __construct($aOptions) + { + parent::__construct(); + + $this->_bIsApi = bx_is_api(); + + $this->_bActionCsrfChecking = true; + + $this->_sObject = $aOptions['object']; + $this->_aOptions = $aOptions; + + $sBrowseParams = bx_get('bp'); + if(!empty($sBrowseParams)) { + $aBrowseParams = bx_process_input(json_decode(urldecode($sBrowseParams), true)); + if(!empty($aBrowseParams) && is_array($aBrowseParams)) + $this->setBrowseParams($aBrowseParams); + } + } + + /** + * Get grid object instance by object name + * @param $sObject object name + * @return object instance or false on error + */ + public static function getObjectInstance($sObject, $oTemplate = false) + { + if (isset($GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject])) + return $GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject]; + + $aObject = BxDolGridQuery::getGridObject($sObject); + if (!$aObject || !is_array($aObject)) + return false; + + $sClass = 'BxTemplGrid'; + if (!empty($aObject['override_class_name'])) { + $sClass = $aObject['override_class_name']; + if (!empty($aObject['override_class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']); + } + + $o = new $sClass($aObject, $oTemplate); + + if (!$o->_isVisibleGrid($aObject)) + return false; + + return ($GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject] = $o); + } + + public function getObject() + { + return $this->_sObject; + } + + /** + * Add replace markers. Curently markers are replaced in 'source' field + * @param $a array of markers as key => value + * @return true on success or false on error + */ + public function addMarkers ($a) + { + if (empty($a) || !is_array($a)) + return false; + $this->_aMarkers = array_merge ($this->_aMarkers, $a); + return true; + } + + public function setBrowseParams($aBrowseParams) + { + $this->_aBrowseParams = $aBrowseParams; + $this->_aQueryAppend['bp'] = urlencode(json_encode($this->_aBrowseParams)); + } + + /** + * Replace provided markers in form array + * @param $a form description array + * @return array where markes are replaced with real values + */ + protected function _replaceMarkers () + { + $this->_aOptions['source'] = bx_replace_markers($this->_aOptions['source'], $this->_aMarkers); + } + + protected function _getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage) + { + $sFunc = '_getData' . $this->_aOptions['source_type']; + return $this->$sFunc($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage); + } + + protected function _getDataArray ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage) + { + if ($this->_aOptions['source'] && !is_array($this->_aOptions['source'])) { + $this->_aOptions['source'] = unserialize($this->_aOptions['source']); + } + + // apply filter + if ($sFilter && (!empty($this->_aOptions['filter_fields']) || !empty($this->_aOptions['filter_fields_translatable']))) { + $aSource = array(); + foreach ($this->_aOptions['source'] as $aRow) { + $bFound = false; + if (!empty($this->_aOptions['filter_fields'])) { + foreach ($this->_aOptions['filter_fields'] as $sField) { + if (empty($aRow[$sField]) || false === stripos($aRow[$sField], $sFilter)) + continue; + $aSource[] = $aRow; + $bFound = true; + break; + } + } + if (!$bFound && !empty($this->_aOptions['filter_fields_translatable'])) { + foreach ($this->_aOptions['filter_fields_translatable'] as $sField) { + if (empty($aRow[$sField]) || false === stripos(_t($aRow[$sField]), $sFilter)) + continue; + $aSource[] = $aRow; + $bFound = true; + break; + } + } + } + } else { + $aSource = &$this->_aOptions['source']; + } + + // sort + $sSortField = false; + $iSortDir = 1; + if ($sOrderField && !empty($this->_aOptions['sorting_fields']) && is_array($this->_aOptions['sorting_fields']) && in_array($sOrderField, $this->_aOptions['sorting_fields'])) { // explicit order + $sSortField = $sOrderField; + $iSortDir = 0 === strcasecmp($sOrderDir, 'desc') ? -1 : 1; + } elseif (!empty($this->_aOptions['field_order'])) { // order by "order" field + $sSortField = $this->_aOptions['field_order']; + } + + if ($sSortField) { + $aSourceOrdered = $aSource; + $this->_tmpOrderField = $sSortField; + $this->_tmpOrderDir = $iSortDir; + usort($aSourceOrdered, array($this, '_cmp')); + } else { + $aSourceOrdered = &$aSource; + } + + // calculate total records count + if ($this->_aOptions['show_total_count'] == 1){ + $this->_iTotalCount = count($aSourceOrdered); + } + return array_slice($aSourceOrdered, $iStart, $iPerPage, true); + } + + protected function _getDataSql ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage) + { + $oDb = BxDolDb::getInstance(); + $sQuery = $this->_aOptions['source']; + if (false === stripos($sQuery, ' WHERE ')) + $sQuery .= " WHERE 1 "; + + $aResults = false; + /** + * @hooks + * @hookdef hook-grid-get_data 'grid', 'get_data' - hook to override the data to be shown in the grid + * - $unit_name - equals `grid` + * - $action - equals `get_data` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `object` - [string] grid object name + * - `options` - [array] grid options array as key&value pairs + * - `markers` - [array] markers array as key&value pairs + * - `filter` - [string] filter value + * - `browse_params` - [array] additional browse params array as key&value pairs + * - `results` - [array] by ref, array of grid rows, where each row is an array of fields values, can be overridden in hook processing + * @hook @ref hook-grid-get_data + */ + bx_alert('grid', 'get_data', 0, false, [ + 'object' => $this->_sObject, + 'options' => $this->_aOptions, + 'markers' => $this->_aMarkers, + 'filter' => $sFilter, + 'browse_params' => $this->_aBrowseParams, + 'results' => &$aResults + ]); + if($aResults !== false) + return $aResults; + + // add filter condition + $sOrderByFilter = ''; + $sQuery .= $this->_getDataSqlWhereClause($sFilter, $sOrderByFilter); + + // calculate total records count + if ($this->_aOptions['show_total_count'] == 1){ + $this->_iTotalCount = $this->_getDataSqlCounter($sQuery, $sFilter); + } + + // add order + $sQuery .= $this->_getDataSqlOrderClause ($sOrderByFilter, $sOrderField, $sOrderDir); + + $sQuery = $sQuery . $oDb->prepareAsString(' LIMIT ?, ?', $iStart, $iPerPage); + return $oDb->getAll($sQuery); + } + + protected function _getDataSqlCounter($sQuery, $sFilter) + { + $oDb = BxDolDb::getInstance(); + + $sQuery = preg_replace("/^SELECT.*FROM/mU", "SELECT COUNT(*) FROM ", $sQuery); + if(strpos($sQuery, 'GROUP BY') === false) + return $oDb->getOne($sQuery); + else + return array_sum($oDb->getColumn($sQuery)); + } + + protected function _getDataSqlWhereClause($sFilter, &$sOrderByFilter) + { + if(!$sFilter || (empty($this->_aOptions['filter_fields']) && empty($this->_aOptions['filter_fields_translatable']))) + return ''; + + $oDb = BxDolDb::getInstance(); + + $sMode = $this->_aOptions['filter_mode']; + if($sMode != 'like' && $sMode != 'fulltext') + $sMode = getParam('useLikeOperator') ? 'like' : 'fulltext'; + + $sCond = ''; + if('like' == $sMode) { // LIKE search + + // condition for regular fields + if (!empty($this->_aOptions['filter_fields'])) + foreach ($this->_aOptions['filter_fields'] as $sField) + $sCond .= $oDb->prepareAsString("`{$sField}` LIKE ? OR ", '%' . $sFilter . '%'); + + // condition for translatable fields + if (!empty($this->_aOptions['filter_fields_translatable'])) { + $sCondFields = ''; + foreach ($this->_aOptions['filter_fields_translatable'] as $sField) + $sCondFields .= "`k`.`Key` = `{$sField}` OR "; + + $sCondFields = rtrim($sCondFields, ' OR '); + + if ($sCondFields) + $sCond .= $oDb->prepareAsString("(SELECT 1 FROM `sys_localization_strings` AS `s` INNER JOIN `sys_localization_keys` AS `k` ON (`k`.`ID` = `s`.`IDKey`) WHERE `s`.`string` LIKE ? AND ($sCondFields) LIMIT 1) OR ", '%' . $sFilter . '%'); + } + + $sCond = rtrim($sCond, ' OR '); + + } + else { // FULLTEXT search + + // condition for regular fields + if (!empty($this->_aOptions['filter_fields'])) { + + $sCondFields = ''; + foreach ($this->_aOptions['filter_fields'] as $sField) + $sCondFields .= "`{$sField}`,"; + + $sCondFields = rtrim($sCondFields, ','); + + if ($sCondFields) { + $sCond = $oDb->prepareAsString(" MATCH ($sCondFields) AGAINST (?) ", $sFilter); + $sOrderByFilter = $sCond; + $sCond .= ' > 1 OR '; + } + } + + // condition for translatable fields + if (!empty($this->_aOptions['filter_fields_translatable'])) { + + $sCondFields = ''; + foreach ($this->_aOptions['filter_fields_translatable'] as $sField) + $sCondFields .= "`k`.`Key` = `{$sField}` OR "; + + $sCondFields = rtrim($sCondFields, ' OR '); + + if ($sCondFields) + $sCond .= $oDb->prepareAsString("(SELECT 1 FROM `sys_localization_strings` AS `s` INNER JOIN `sys_localization_keys` AS `k` ON (`k`.`ID` = `s`.`IDKey`) WHERE MATCH (`s`.`string`) AGAINST (?) AND ($sCondFields) LIMIT 1) OR ", $sFilter); + } + + $sCond = rtrim($sCond, ' OR '); + } + + /** + * @hooks + * @hookdef hook-grid-get_data_by_filter 'grid', 'get_data_by_filter' - hook to override the data to be shown in the grid + * - $unit_name - equals `grid` + * - $action - equals `get_data_by_filter` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `object` - [string] grid object name + * - `options` - [array] grid options array as key&value pairs + * - `markers` - [array] markers array as key&value pairs + * - `filter` - [string] filter value + * - `browse_params` - [array] additional browse params array as key&value pairs + * - `conditions` - [string] by ref, 'where' part of SQL query in accordance with provided filter(s), can be overridden in hook processing + * @hook @ref hook-grid-get_data_by_filter + */ + bx_alert('grid', 'get_data_by_filter', 0, false, [ + 'object' => $this->_sObject, + 'options' => $this->_aOptions, + 'markers' => $this->_aMarkers, + 'filter' => $sFilter, + 'browse_params' => $this->_aBrowseParams, + 'conditions' => &$sCond + ]); + + return $sCond ? ' AND (' . $sCond . ')' : $sCond; + } + + protected function _getDataSqlOrderClause ($sOrderByFilter, $sOrderField, $sOrderDir, $bFieldsOnly = false) + { + $sOrderClause = ''; + + if ($sOrderField && is_array($this->_aOptions['sorting_fields']) && in_array($sOrderField, $this->_aOptions['sorting_fields'])) { // explicit order + + $sDir = (0 === strcasecmp($sOrderDir, 'desc') ? 'DESC' : 'ASC'); + + if (is_array($this->_aOptions['sorting_fields_translatable']) && in_array($sOrderField, $this->_aOptions['sorting_fields_translatable'])) { + + // translatable fields + $iLang = BxDolLanguages::getInstance()->getCurrentLangId(); + $oDb = BxDolDb::getInstance(); + $sOrderClause = $oDb->prepareAsString("(SELECT `s`.`string` FROM `sys_localization_strings` AS `s` INNER JOIN `sys_localization_keys` AS `k` ON (`k`.`ID` = `s`.`IDKey`) WHERE `k`.`KEY` = `$sOrderField` AND `s`.`IDLanguage` = ? LIMIT 1) ", $iLang) . $sDir; + + } else { + + // regular fields + $sOrderClause = "`" . $sOrderField . "` $sDir"; + + } + + } elseif ($sOrderByFilter) { // order by filter + + $sOrderClause = $sOrderByFilter . " DESC"; + + } elseif (!empty($this->_aOptions['field_order'])) { // order by "order" field + + if (false == strpos($this->_aOptions['field_order'], ',')) { + $sOrderClause = "`" . $this->_aOptions['field_order'] . "` " . $this->_sDefaultSortingOrder; + } else { + $a = explode(',', $this->_aOptions['field_order']); + foreach ($a as $sField) + $sOrderClause .= "`" . trim($sField) . "` " . $this->_sDefaultSortingOrder . ", "; + + if ($sOrderClause) + $sOrderClause = trim($sOrderClause, ', '); + } + + } + + return $bFieldsOnly || empty($sOrderClause) ? $sOrderClause : " ORDER BY " . $sOrderClause; + } + + protected function _getCellData($sKey, $aField, $aRow) + { + if (isset($aRow[$sKey])) { + if (!empty($aField['display'])) { + bx_import('BxDolForm'); + $sDisplayFunc = 'display' . $aField['display']; + $oDisplay = new BxDolFormCheckerHelper(); + return $oDisplay->$sDisplayFunc($aRow[$sKey]); + } else { + return bx_process_output($aRow[$sKey]); + } + } else { + return _t('_undefined'); + } + } + + protected function _cmp ($r1, $r2) + { + $iRet = strcasecmp($r1[$this->_tmpOrderField], $r2[$this->_tmpOrderField]); + return $iRet ? $this->_tmpOrderDir * $iRet : 0; + } + + protected function _genMethodName ($s) + { + return bx_gen_method_name($s); + } + + protected function _isVisibleGrid ($a) + { + if (isAdmin() || !isset($a['visible_for_levels'])) + return true; + return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']); + } + + protected function _getFilterValue() + { + return bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['filter_get']))); + } + + protected function _getOrderValue() + { + return bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['order_get_field']))); + } + + public function setActionCsrfChecking($bCsrfChecking) + { + $this->_bActionCsrfChecking = $bCsrfChecking; + } + + public function isActionCsrfCheckingDisabled() { + return !$this->_bActionCsrfChecking; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMenu.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMenu.php new file mode 100644 index 0000000000..5c89bba6fe --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMenu.php @@ -0,0 +1,540 @@ + 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 $_bIsApi; + + protected $_bHx; + protected $_bHxHead; + protected $_mHxPreload; + protected $_aHx; + + protected $_bDynamicMode; + protected $_bAddNoFollow; + + protected $_bSelModuleCheck; + protected $_sSelModule; + protected $_sSelName; + + protected $_sObject; + protected $_aObject; + protected $_oQuery; + protected $_oPermalinks; + protected $_aMarkers = array(); + protected $_bMultilevel = false; + + protected $_sSessionKeyCollapsed; + + protected $_aContentParams; + + /** + * Constructor + * @param $aObject array of menu options + */ + protected function __construct($aObject) + { + parent::__construct(); + + $this->_bIsApi = bx_is_api(); + + $this->_bHx = false; + $this->_aHx = []; + + $this->_bDynamicMode = false; + $this->_bAddNoFollow = getParam('sys_add_nofollow') == 'on'; + + $this->_bSelModuleCheck = 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(); + + $this->_bMultilevel = !empty($this->_aObject['set_name']) && $this->_oQuery->isSetMultilevel($this->_aObject['set_name']); + + $this->_sSessionKeyCollapsed = 'bx_menu_collapsed_'; + + $this->_aContentParams = []; + + $this->addMarkers([ + 'object' => $this->_sObject + ]); + + if(isLogged() && ($oProfile = BxDolProfile::getInstance()) !== false) { + $sUrl = $oProfile->getUrl(); + if($this->_bIsApi) + $sUrl = bx_api_get_relative_url($sUrl); + + $this->addMarkers([ + 'member_id' => $oProfile->id(), + 'member_display_name' => $oProfile->getDisplayName(), + 'member_url' => $sUrl, + 'content_id' => $oProfile->getContentId() + ]); + } + } + + /** + * Get menu object instance by object name + * @param $sObject object name + * @return object instance or false on error + */ + static public function getObjectInstance($sObject, $oTemplate = false) + { + $oMenu = false; + if (!isset($GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject])) { + $aObject = BxDolMenuQuery::getMenuObject($sObject); + if (!$aObject || !is_array($aObject) || (int)$aObject['active'] == 0) + return false; + + $sClass = 'BxTemplMenu'; + if (!empty($aObject['override_class_name'])) { + $sClass = $aObject['override_class_name']; + if (!empty($aObject['override_class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']); + } + + $oMenu = new $sClass($aObject, $oTemplate); + $GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject] = $oMenu; + } + else + $oMenu = $GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject]; + + /** + * @hooks + * @hookdef hook-system-get_object 'system', 'get_object' - hook to override menu object + * - $unit_name - equals `system` + * - $action - equals `get_object` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `type` - [string] object type, equals to 'menu' + * - `name` - [string] menu object name + * - `object` - [object] by ref, an instance of menu, @see BxDolMenu, can be overridden in hook processing + * @hook @ref hook-system-get_object + */ + bx_alert('system', 'get_object', 0, false, [ + 'type' => 'menu', + 'name' => $sObject, + 'object' => &$oMenu, + ]); + + return $oMenu; + } + + /** + * Set selected menu item globally. + * @param $sModule menu item module to set as selected + * @param $sName menu item name to set as selected + */ + static public function setSelectedGlobal ($sModule, $sName) + { + self::$SEL_MODULE = $sModule; + self::$SEL_NAME = $sName; + } + + /** + * Process menu triggers. + * Menu triggers allow to automatically add menu items to modules with no different if dependant module was install before or after the module menu item belongs to. + * For example module "Notes" adds menu items to all profiles modules (Persons, Organizations, etc) + * with no difference if persons module was installed before or after "Notes" module was installed. + * @param $sMenuTriggerName trigger name to process, usually specified in module installer class - @see BxBaseModGeneralInstaller + * @return always true, always success + */ + static public function processMenuTrigger ($sMenuTriggerName) + { + // get list of active modules + $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array( + 'type' => 'modules', + 'active' => 1, + )); + + // get list of menu triggers + $aMenuItems = BxDolMenuQuery::getMenuTriggers($sMenuTriggerName); + + // check each menu item trigger for all modules + foreach ($aMenuItems as $aMenuItem) { + foreach ($aModules as $aModule) { + if (!BxDolRequest::serviceExists($aModule['name'], 'get_menu_set_name_for_menu_trigger')) + continue; + + $mixedMenuSet = BxDolService::call($aModule['name'], 'get_menu_set_name_for_menu_trigger', array($sMenuTriggerName)); + if(empty($mixedMenuSet)) + continue; + + if(is_string($mixedMenuSet)) + $mixedMenuSet = array($mixedMenuSet); + + foreach($mixedMenuSet as $sMenuSet) { + if(empty($sMenuSet)) + continue; + + $aMenuItem['set_name'] = $sMenuSet; + BxDolMenuQuery::addMenuItemToSet($aMenuItem); + } + } + } + + return true; + } + + public function isHtmx() + { + return $this->_bHx; + } + + /** + * Check if the menu is visible. The menu is visible if at least one menu item is visible. + * @return boolean + */ + public function isVisible() + { + if((int)$this->_aObject['active'] == 0) + return false; + + if(!isset($this->_aObject['menu_items'])) + $this->_aObject['menu_items'] = $this->_oQuery->getMenuItems(); + + $bVisible = false; + foreach ($this->_aObject['menu_items'] as $a) { + if(!$this->_isActive($a) || !$this->_isVisible($a)) + continue; + + $bVisible = true; + break; + } + + return $bVisible; + } + + public function getTemplateId() + { + return $this->_aObject['template_id']; + } + + /** + * Get template name with checking for custom template related to exactly this menu object. + * @return string with template name. + */ + public function getTemplateName($sName = '') + { + if(empty($sName)) + $sName = $this->_aObject['template']; + + $sNameCustom = str_replace('.html', '_' . $this->_sObject . '.html', $sName); + return $this->_oTemplate->isHtml($sNameCustom) ? $sNameCustom : $sName; + } + + public function setTemplateById ($iTemplateId) + { + $aTemplate = $this->_oQuery->getMenuTemplateById($iTemplateId); + if(empty($aTemplate) || !is_array($aTemplate)) + return; + + $this->_aObject['template'] = $aTemplate['template']; + } + + /** + * Set selected menu item for current menu object only. + * @param $sModule menu item module to set as selected + * @param $sName menu item name to set as selected + */ + public function setSelected ($sModule, $sName) + { + $this->_sSelModule = $sModule; + $this->_sSelName = $sName; + } + + public function setDynamicMode ($bDynamicMode) + { + $this->_bDynamicMode = $bDynamicMode; + } + + public function setHtmx($bHx) + { + $this->_bHx = $bHx; + } + + /** + * Get an arrey of replacable markers. + * @return an array with markers + */ + public function getMarkers() + { + return $this->_aMarkers; + } + + /** + * Add replace markers. + * @param $a array of markers as key => value + * @return true on success or false on error + */ + public function addMarkers ($a) + { + if (empty($a) || !is_array($a)) + return false; + $this->_aMarkers = array_merge ($this->_aMarkers, $a); + return true; + } + + /** + * Remove marker + * @param $s marker key + */ + public function removeMarker ($s) + { + unset($this->_aMarkers[$s]); + } + + public function initContentParams() + { + $this->_aContentParams = []; + } + + public function setContentParams($aParams) + { + $this->_aContentParams = $aParams; + + return true; + } + + public function getContentParams() + { + if(!$this->_aContentParams) + $this->initContentParams(); + + return $this->_aContentParams; + } + + public function performActionSetCollapsed($mixedValue) + { + $this->_setCollapsed($this->_sObject, (int)$mixedValue); + } + + public function performActionSetCollapsedSubmenu($sMenuItem, $mixedValue) + { + $this->_setCollapsed($this->_sObject . '_' . $sMenuItem, (int)$mixedValue); + } + + public function getUserChoiceCollapsed($sObject = '') + { + $iProfile = bx_get_logged_profile_id(); + if(!$iProfile) + return false; + + if(!$sObject) + $sObject = $this->_sObject; + + $sSessionKey = $this->_sSessionKeyCollapsed . $iProfile; + $aCollapsed = BxDolSession::getInstance()->getValue($sSessionKey); + if(!isset($aCollapsed[$sObject])) + return false; + + return (int)$aCollapsed[$sObject]; + } + + public function getUserChoiceCollapsedSubmenu($mixedItem, $sObject = '') + { + if(!$sObject) + $sObject = $this->_sObject; + + if(is_array($mixedItem) && isset($mixedItem['name'])) + $sObject .= '_' . $mixedItem['name']; + else if(is_string($mixedItem)) + $sObject .= '_' . $mixedItem; + + return $this->getUserChoiceCollapsed($sObject); + } + + protected function _setCollapsed($sName, $mixedValue) + { + $iProfile = bx_get_logged_profile_id(); + if(!$iProfile) + return; + + $oSession = BxDolSession::getInstance(); + $sSessionKey = $this->_sSessionKeyCollapsed . $iProfile; + + $aCollapsed = $oSession->getValue($sSessionKey); + if(!is_array($aCollapsed)) + $aCollapsed = []; + + $aCollapsed[$sName] = $mixedValue; + $oSession->setValue($sSessionKey, $aCollapsed); + } + + /** + * Check if menu items is selected. + * @param $a menu item array + * @return boolean + */ + protected function _isSelected ($a) + { + if($this->_sSelModule || $this->_sSelName) + return (!$this->_bSelModuleCheck || !isset($a['module']) || $a['module'] == $this->_sSelModule) && (isset($a['name']) && $a['name'] == $this->_sSelName) ? true : false; + + return (!$this->_bSelModuleCheck || !isset($a['module']) || $a['module'] == self::$SEL_MODULE) && (isset($a['name']) && $a['name'] == self::$SEL_NAME) ? true : false; + } + + /** + * Check if menu items is active. + * @param $a menu item array + * @return boolean + */ + protected function _isActive ($a) + { + if($this->_bIsApi) + return !isset($a['active_api']) || (int)$a['active_api'] !=0; + else + return !isset($a['active']) || (int)$a['active'] != 0; + } + + /** + * Check if menu items is visible. + * @param $a menu item array + * @return boolean + */ + protected function _isVisible ($a) + { + if(isset($a['visible_for_levels']) && !BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels'])) + return false; + + if(!empty($a['visibility_custom']) && !BxDolService::callSerialized($a['visibility_custom'], $this->_aMarkers)) + return false; + + if($this->_iPageType && !empty($a['hidden_on_pt']) && ((1 << ($this->_iPageType - 2)) & (int)$a['hidden_on_pt'])) + return false; + + return true; + } + + protected function _getVisibilityClass($a) + { + $aHiddenOn = array( + pow(2, BX_DB_HIDDEN_PHONE - 1) => 'bx-def-media-phone-hide', + pow(2, BX_DB_HIDDEN_TABLET - 1) => 'bx-def-media-tablet-hide', + pow(2, BX_DB_HIDDEN_DESKTOP - 1) => 'bx-def-media-desktop-hide', + pow(2, BX_DB_HIDDEN_MOBILE - 1) => 'bx-def-mobile-app-hide' + ); + + $aHiddenOnCol = array( + pow(2, 1) => 'bx-def-thin-col-hide', + pow(2, 2) => 'bx-def-half-col-hide', + pow(2, 3) => 'bx-def-wide-col-hide', + pow(2, 4) => 'bx-def-full-col-hide' + ); + + $sHiddenOnCssClasses = ''; + if(!empty($a['hidden_on'])) + foreach($aHiddenOn as $iHiddenOn => $sClass) + if((int)$a['hidden_on'] & $iHiddenOn) + $sHiddenOnCssClasses .= ' ' . $sClass; + + + if(!empty($a['hidden_on_col'])){ + foreach($aHiddenOnCol as $iHiddenOn => $sClass) + if((int)$a['hidden_on_col'] & $iHiddenOn) + $sHiddenOnCssClasses .= ' ' . $sClass; + } + + return $sHiddenOnCssClasses; + } + + /** + * 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; + } + +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMetatags.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMetatags.php new file mode 100644 index 0000000000..5fcd7a148c --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolMetatags.php @@ -0,0 +1,1212 @@ +locationsAddFromForm($iContentId); + * @endcode + * + * Upon 'edit' form display call BxDolMetatags::locationGet to fill-in location form input, for example: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $aSpecificValues = $oMetatags->locationGet($iContentId); + * $oForm->initChecker($aContentInfo, $aSpecificValues); + * @endcode + * + * To display location call BxDolMetatags::locationsString: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * echo $oMetatags->locationsString($iContentId) + * @endcode + * + * Upon page display call BxDolMetatags::addPageMetaInfo to add location (and all other, including content image) meta information to the page header: + * @code + * $o = BxDolMetatags::getObjectInstance('object_name'); + * $o->addPageMetaInfo($iContentId, array('id' => $iFileId, 'object' => 'storage_object_name')); + * @endcode + * + * Form object must have location field, usual name for this field is 'location' (this name is used as $sPrefix in some functions), type is 'custom', + * also form object must be derived from BxBaseModGeneralFormEntry class, which has custom field declaration. + * + * + * + * @section metatags_keywords \#keyword + * + * Upon content 'add' and 'edit' form submit, call BxDolMetatags::keywordsAdd + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $oMetatags->keywordsAdd($iContentId, $aContentInfo['text_field_with_hash_tags']); + * @endcode + * + * Before displaying the text with hashtags call BxDolMetatags::keywordsParse to highlight keywords: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $sText = $oMetatags->keywordsParse($iContentId, $sText); + * @endcode + * + * Upon page display call BxDolMetatags::addPageMetaInfo to add keywords (and all other, including content image) meta information to the page header. + * + * To be able to search by tags metatgs object must be specified in *SearchResult class as 'object_metatags' in 'aCurrent' array. + * + * + * + * @section metatags_mention \@mention + * + * Upon content 'add' and 'edit' form submit, call BxDolMetatags::mentionsAdd + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $oMetatags->mentionsAdd($iContentId, $aContentInfo['text_field_with_mentions']); + * @endcode + * + * Before displaying the text with mentions call BxDolMetatags::mentionsParse: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $sText = $oMetatags->mentionsParse($iContentId, $sText); + * @endcode + * + * Upon page display call BxDolMetatags::addPageMetaInfo to add all available meta info to the page meta into where possible, however it doesn't adding anyting for mentions for now. + * + * To be able to search by mentions object must be specified in *SearchResult class as 'object_metatags' in 'aCurrent' array. + * + * + * + * @section metatags_auto Add \@mention, \#keyword using one call + * + * Upon content 'add' and 'edit' form submit, call BxDolMetatags::metaAdd + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $oMetatags->metaAdd($iContentId, $aContentInfo['text_field']); + * @endcode + * or call BxDolMetatags::metaAddAuto, it autodetects fields with metainfo: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $oMetatags->metaAddAuto($iContentId, $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']); + * @endcode + * + * Before displaying the text with different metainfo call BxDolMetatags::metaParse: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $sText = $oMetatags->metaParse($iContentId, $sText); + * @endcode + * + * Upon page display call BxDolMetatags::addPageMetaInfo to add all available meta info to the page meta into where possible. + * + * + * + * @section metatags_delete_content Content deletion + * + * When content is deleted associated meta data can be deleted by calling BxDolMetatags::onDeleteContent: + * @code + * $oMetatags = BxDolMetatags::getObjectInstance('object_name'); + * $oMetatags->onDeleteContent($iContentId); + * @endcode + * + */ +class BxDolMetatags extends BxDolFactory implements iBxDolFactoryObject +{ + protected static $_aLocationKeys = ['lat', 'lng', 'country', 'state', 'city', 'zip', 'street', 'street_number']; + protected static $_sKeywordPattern = '/[\pCc\pZ\p{Ps}\p{Pe}\p{Pi}\p{Pf}]\#(\pL[\pL\pN_]+)/u'; + + protected $_sObject; + protected $_aObject; + protected $_oQuery; + protected $_aMetas = []; + + /** + * 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); + } + + /** + * Get metatags by term + * @param string $sMeta - 'keywords' is only supported meta for now + * @param type $sMetaItem + * @param type $sTerm + * @return type + */ + static public function getMetatagsDataByTerm($sMeta, $sMetaItem, $sTerm) + { + $bHashtagsOnly = getParam('sys_metatags_hashtags_only') == 'on'; + $aLabels = []; + if($bHashtagsOnly) + $aLabels = array_map(function($sValue) {return mb_strtolower($sValue);}, BxDolLabel::getInstance()->getLabels(['type' => 'values'])); + + $aValues = []; + $aObjects = BxDolMetatagsQuery::getMetatagsObjects(); + foreach($aObjects as $aObject) { + $oObject = BxDolMetatags::getObjectInstance($aObject['object']); + if(!$oObject || !$oObject->{$sMeta . 'IsEnabled'}()) + continue; + + $sMethod = $sMeta . 'GetByTerm'; + if(!method_exists($oObject->_oQuery, $sMethod)) + continue; + + $aData = $oObject->_oQuery->$sMethod($sTerm); + foreach($aData as $aMeta) { + if($bHashtagsOnly && in_array(strtolower($aMeta[$sMetaItem]), $aLabels)) + continue; + + $aMatch = []; + if(!preg_match(self::$_sKeywordPattern, ' #' . $aMeta[$sMetaItem], $aMatch) || !isset($aMatch[1]) || strcasecmp($aMeta[$sMetaItem], $aMatch[1]) != 0) + continue; + + $aValues[$aMeta[$sMetaItem]] = [ + 'id' => $aMeta['object_id'], + 'meta' => $aMeta[$sMetaItem], + 'url' => $oObject->keywordsGetHashTagUrl($aMeta[$sMetaItem], $aMeta['object_id']) + ]; + } + } + + $aValues = array_slice($aValues, 0, (int)getParam('sys_profiles_search_limit')); + + return $aValues; + } + + public function getModule() + { + return $this->_aObject['module']; + } + + /** + * Add all available meta tags to the head section + * @return number of successfully added metas + */ + public function addPageMetaInfo($iId, $mixedImage = false) + { + $i = 0; + foreach ($this->_aMetas as $sMeta) { + $sFunc = $sMeta . 'AddMeta'; + $i += $this->$sFunc($iId); + } + + if ($mixedImage && is_array($mixedImage)) { + + if (!empty($mixedImage['object'])) + $o = BxDolStorage::getObjectInstance($mixedImage['object']); + elseif (!empty($mixedImage['transcoder'])) + $o = BxDolTranscoder::getObjectInstance($mixedImage['transcoder']); + + $mixedImage = $o ? $o->getFileUrlById($mixedImage['id']) : false; + } + + if ($mixedImage) + BxDolTemplate::getInstance()->addPageMetaImage($mixedImage); + + return $i; + } + + /** + * Perform @see keywordsParse @see mentionsParse and maybe other + * @param $iId content id + * @param $s string + * @return modified string + */ + public function metaParse($iId, $s) + { + foreach ($this->_aMetas as $sMeta) { + $sFunc = $sMeta . 'Parse'; + if (method_exists($this, $sFunc)) + $s = $this->$sFunc($iId, $s); + } + return $s; + } + + /** + * Perform @see keywordsAddAuto @see mentionsAddAuto and maybe other + * @param $iId content id + * @param $aContentInfo content info array + * @param $CNF module config array + * @param $sFormDisplay form display object + * @return number of founded keywords, mentions, etc + */ + public function metaAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) + { + $i = 0; + foreach ($this->_aMetas as $sMeta) { + $sFunc = $sMeta . 'AddAuto'; + if (method_exists($this, $sFunc)) + $i += $this->$sFunc($iId, $aContentInfo, $CNF, $sFormDisplay); + } + return $i; + } + + /** + * Perform @see keywordsAdd @see mentionsAdd and maybe other + * @param $iId content id + * @param $s string + * @return number of added keywords, mentions, etc + */ + public function metaAdd($iId, $s) + { + $i = 0; + foreach ($this->_aMetas as $sMeta) { + if ('locations' == $sMeta) + continue; + $sFunc = $sMeta . 'Add'; + if (method_exists($this, $sFunc)) + $i += $this->$sFunc($iId, $s); + } + return $i; + } + + /** + * Checks if keywords enabled for current metatags object + */ + public function keywordsIsEnabled() + { + return empty($this->_aObject['table_keywords']) ? false : true; + } + + /** + * This function is specifically for formatting "Photo Camera" string, when "Photo Camera" is used as image hashtag + */ + static public function keywordsCameraModel($aExif) + { + if (!isset($aExif['Make'])) + return ''; + + $sMake = trim($aExif['Make']); + if ($sMake && isset($aExif['Model'])) { + $sModel = trim($aExif['Model']); + if (0 === mb_strpos($sModel, $sMake)) + $sModel = mb_substr($sModel, mb_strlen($sMake)); + } + + return $sMake . (empty($sModel) ? '' : ' ' . trim($sModel)); + } + + /** + * Add \#keywords from the string + * @param $iId content id + * @param $s string with \#keywords + * @return number of found keywords + */ + public function keywordsAdd($iId, $s) + { + //--- Remove tags WITH their content first. + $s = preg_replace("/])*>(.*?)<\/a>/si", '', $s); + + //--- Strip the other HTML tags. + $s = strip_tags(str_replace(array('
', '
', '
', '
', '

', '', '', '', '', '', ''), "\n", $s)); + + // process spaces + $s = str_ireplace(' ', ' ', $s); + + return $this->_metaAdd($iId, ' ' . $s, self::$_sKeywordPattern, 'keywordsDelete', 'keywordsAdd', 'keywordsGet', (int)getParam('sys_metatags_hashtags_max'), 'keyword'); + } + + /** + * Add keyword from the whole string + * @param $iId content id + * @param $s keyword + * @return number of added keywords, should be 1 + */ + public function keywordsAddOne($iId, $s, $bDeletePreviousKeywords = true) + { + if ($iRet = $this->_oQuery->keywordsAdd($iId, array($s), $bDeletePreviousKeywords)) { + $sSource = $this->_sObject . '_' . $iId; + + /** + * @hooks + * @hookdef hook-bx_dol_metatags-keyword_added '{object_name}', 'keyword_added' - hook after a keyword (hashtag) was recognized in provided text + * - $unit_name - metatags object name + * - $action - equals `keyword_added` + * - $object_id - content id + * - $sender_id - logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `meta` - [string] recognized keyword + * - `content_id` - [int] content id + * - `source` - [string] unique source id + * @hook @ref hook-bx_dol_metatags-keyword_added + */ + bx_alert($this->_aObject['module'], 'keyword_added', $iId, bx_get_logged_profile_id(), [ + 'meta' => $s, + 'content_id' => $iId, + 'source' => $sSource + ]); + + /** + * @hooks + * @hookdef hook-meta_keyword-added 'meta_keyword', 'added' - hook after a keyword (hashtag) was recognized in provided text + * It's equivalent to @ref hook-bx_dol_metatags-keyword_added + * except `object` parameter with metatags object name was added in $extra_params + * @hook @ref hook-meta_keyword-added + */ + bx_alert('meta_keyword', 'added', $iId, bx_get_logged_profile_id(), [ + 'meta' => $s, + 'content_id' => $iId, + 'object' => $this->_sObject, + 'source' => $sSource + ]); + } + + return $iRet; + } + + /** + * Add \#keywords from the content fields + * @param $iId content id + * @param $aContentInfo content info array + * @param $CNF module config array + * @param $sFormDisplay form display object + * @return number of found keywords + */ + public function keywordsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) + { + $aFields = $this->metaFields($aContentInfo, $CNF, $sFormDisplay); + $sTextWithKeywords = ''; + foreach ($aFields as $sField) + $sTextWithKeywords .= "\n" . $aContentInfo[$sField]; + + return $this->keywordsAdd($iId, $sTextWithKeywords); + } + + /** + * Get field names which are subject to parse keywords + */ + public function metaFields($aContentInfo, $CNF, $sFormDisplay, $bHtmlOnly = false) + { + $aFields = array(); + if (empty($CNF['FIELDS_WITH_KEYWORDS'])) { + return array(); + } + elseif (is_string($CNF['FIELDS_WITH_KEYWORDS']) && 'auto' == $CNF['FIELDS_WITH_KEYWORDS']) { + if (!($oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sFormDisplay))) + return array(); + + foreach ($oForm->aInputs as $k => $a) { + if ('textarea' == $a['type'] && (!$bHtmlOnly || ($bHtmlOnly && $a['html']))) + $aFields[] = $a['name']; + } + } + elseif (is_array($CNF['FIELDS_WITH_KEYWORDS'])) { + $aFields = $CNF['FIELDS_WITH_KEYWORDS']; + } + elseif (is_string($CNF['FIELDS_WITH_KEYWORDS'])) { + $aFields = explode(',', $CNF['FIELDS_WITH_KEYWORDS']); + } + + return $aFields; + } + + /** + * Add links to the \#keywords in the string + * @param $iId content id + * @param $s string with \#keywords + * @return modified string where all \#keywords are transformed to links with rel="tag" attribute + */ + public function keywordsParse($iId, $s) + { + $a = $this->keywordsGet($iId); + if (empty($a)) + return $s; + + foreach ($a as $sKeyword) { + $f = function ($a) use ($sKeyword, $iId) { + return $a[1] . '
'; + }; + + $s = preg_replace_callback('/([^\pN^\pL])\#(' . preg_quote($sKeyword, '/') . ')/u', $f, $s); + $s = preg_replace_callback('/^()\#(' . preg_quote($sKeyword, '/') . ')/u', $f, $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 = ''; + + return $s; + } + + public function keywordsGetHashTagUrl($sKeyword, $iId, $mixedSection = false) + { + $sSectionPart = ''; + if (!empty($mixedSection)) { + if (is_array($mixedSection)) + $sSectionPart = '§ion[]=' . implode('§ion[]=', $mixedSection); + elseif (is_string($mixedSection)) + $sSectionPart = '§ion[]=' . $mixedSection; + } + + $sUrl = BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . $sSectionPart; + + /** + * @hooks + * @hookdef hook-meta_keyword-url 'meta_keyword', 'url' - hook to override meta keyword URL + * - $unit_name - equals `meta_keyword` + * - $action - equals `url` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `url` - [string] by ref, keyword URL, can be overridden in hook processing + * - `keyword` - [string] keyword + * - `id` - [int] content id + * - `object` - [string] metatags object name + * - `section` - [string] a string with sections (modules) in which a search by keyword will be performed (for default hashtag URL) + * @hook @ref hook-meta_keyword-url + */ + bx_alert('meta_keyword', 'url', 0, false, [ + 'url' => &$sUrl, + 'keyword' => $sKeyword, + 'id' => $iId, + 'object' => $this->_sObject, + 'section' => $mixedSection, + 'sObject' => $this->_sObject, //depricated, will be removed in future versions, approximately in UNA 15. + ]); + + return bx_is_api() ? bx_api_get_relative_url($sUrl) : $sUrl; + } + + /** + * Add keywords meta info to the head section + * @param $iId content id + */ + protected function keywordsAddMeta($iId) + { + BxDolTemplate::getInstance()->addPageKeywords($this->keywordsGet($iId)); + } + + /** + * Get list of keywords associated with the content + * @return array of keywords + */ + public function keywordsGet($iId) + { + return $this->_oQuery->keywordsGet($iId); + } + + /** + * Set condition for search results object for meta keyword + * @param $oSearchResult search results object + * @param $sKeyword keyword + */ + public function keywordsSetSearchCondition($oSearchResult, $sKeyword, $iCmtsSystemId = 0) + { + if (!$this->keywordsIsEnabled()) + return; + + if ('sys_cmts' == $oSearchResult->aCurrent['object_metatags']) { + $this->keywordsSetSearchConditionCmts($oSearchResult, $sKeyword, $iCmtsSystemId); + return; + } + + $oSearchResult->aCurrent['restriction']['meta_keyword'] = array( + 'value' => $sKeyword, + 'field' => 'keyword', + 'operator' => '=', + 'table' => $this->_aObject['table_keywords'], + ); + + $oSearchResult->aCurrent['join']['meta_keyword'] = array( + 'type' => 'INNER', + 'table' => $this->_aObject['table_keywords'], + 'mainField' => $oSearchResult->aCurrent['ident'], + 'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'], + 'onField' => 'object_id', + 'joinFields' => array(), + ); + } + + public function keywordsSetSearchConditionCmts($oSearchResult, $sKeyword, $iCmtsSystemId = 0) + { + if (!$this->keywordsIsEnabled()) + return; + + if ($iCmtsSystemId) { + $oSearchResult->aCurrent['restriction']['meta_keyword_cmts_system'] = array( + 'value' => $iCmtsSystemId, + 'field' => 'system_id', + 'operator' => '=', + 'table' => 'sys_cmts_ids', + ); + } + + $oSearchResult->aCurrent['restriction']['meta_keyword'] = array( + 'value' => $sKeyword, + 'field' => 'keyword', + 'operator' => '=', + 'table' => $this->_aObject['table_keywords'], + ); + + $oSearchResult->aCurrent['join']['meta_keyword_cmts'] = array( + 'type' => 'INNER', + 'table' => 'sys_cmts_ids', + 'mainField' => $oSearchResult->aCurrent['ident'], + 'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'], + 'onField' => 'cmt_id', + 'joinFields' => array(), + ); + + $oSearchResult->aCurrent['join']['meta_keyword'] = array( + 'type' => 'INNER', + 'table' => $this->_aObject['table_keywords'], + 'mainField' => 'id', + 'mainTable' => 'sys_cmts_ids', + 'onField' => 'object_id', + 'joinFields' => array(), + ); + } + + /** + * Get part of SQL query for meta keyword + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $sKeyword keyword + */ + public function keywordsGetAsSQLPart($sContentTable, $sContentField, $sKeyword) + { + if (empty($this->_aObject['table_keywords'])) + return array(); + + return call_user_func_array(array($this->_oQuery, 'keywordsGetSQLParts'), func_get_args()); + } + + public function keywordsPopularList($iLimit, $mContextId = false) + { + return $this->_oQuery->keywordsPopularList($iLimit, $mContextId); + } + + + /** + * Checks if locations enabled for current metatags object + */ + public function locationsIsEnabled() + { + return empty($this->_aObject['table_locations']) ? false : true; + } + + /** + * Add location for the content + * @param $iId content id + * @param $sLatitude latitude + * @param $sLongitude longitude + * @param $sCountryCode optional 2 letters country code (ISO 3166-1) + * @param $sState optional state/province/territory name + * @param $sCity optional city name + * @param $sZip optional ZIP/postcode + * @param $sStreet optional street name + * @param $sStreetNumber optional street number + * @return true if location was added, or false otherwise + */ + public function locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip = '', $sStreet = '', $sStreetNumber = '') + { + // TODO: if lat & lng aren't defined then perform geocoding automatically, or maybe leverage this on client side ? + + return $this->_oQuery->locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber); + } + + /** + * Retrieve location for the content from POST data + * @param $sPrefix field prefix for POST data, or empty - if no prefix + * @param $oForm form to use to get POST data, or null - then new form instance will be created + */ + public static function locationsRetrieveFromForm($sPrefix = '', $oForm = null) + { + if($sPrefix) + $sPrefix .= '_'; + + if(!$oForm) + $oForm = new BxDolForm(array(), false); + + $aResults = []; + foreach(self::$_aLocationKeys as $sKey) { + $sValue = $oForm->getCleanValue($sPrefix . $sKey); + if(!$sValue || $sValue == 'null') + $sValue = ''; + + $aResults[] = $sValue; + } + + return $aResults; + } + + /** + * Parse address components into associative array with the following indexes: + * lat, lng, country, state, city, zip, street, street_number + */ + public static function locationsParseComponents($aAdress, $sPrefix = '') + { + if($sPrefix) + $sPrefix .= '_'; + + $aRet = array(); + $iAdress = count($aAdress); + for($i = 0; $i < $iAdress; $i++) + if(isset(self::$_aLocationKeys[$i])) + $aRet[$sPrefix . self::$_aLocationKeys[$i]] = $aAdress[$i]; + + return $aRet; + } + + /** + * Parse google's formatted address components into array with the following indexes: + * lat, lng, country, state, city, zip, street, street_number + */ + public static function locationsParseAddressComponents($aAdress, $sPrefix = '') + { + if (!isset($aAdress['address_components'])) + return false; + if (!isset($aAdress['geometry']['location'])) + return false; + + $aRet = array( + $sPrefix . 'lat' => $aAdress['geometry']['location']['lat'], + $sPrefix . 'lng' => $aAdress['geometry']['location']['lng'], + ); + + $aMap = array( + $sPrefix . 'city' => 'locality', + $sPrefix . 'state' => 'administrative_area_level_1', + $sPrefix . 'country' => 'country', + $sPrefix . 'zip' => 'postal_code', + $sPrefix . 'street' => 'route', + $sPrefix . 'street_number' => 'street_number' + ); + + $aAdressComponents = $aAdress['address_components']; + + foreach ($aAdressComponents as $r) { + if (!isset($r['types'])) + continue; + foreach ($aMap as $sKey => $sName) { + + if ('locality' == $sName && !in_array($sName, $r['types'])) + $sName = 'postal_town'; + + if ('administrative_area_level_1' == $sName && !in_array($sName, $r['types'])) + $sName = 'administrative_area_level_2'; + + if (in_array($sName, $r['types'])) { + $sIndex = 'route' == $sName || 'locality' == $sName ? 'long_name' : 'short_name'; + $aRet[$sKey] = $r[$sIndex]; + } + + if (in_array('locality', $r['types'])) + $aRet[$sPrefix . 'city'] = $r['short_name']; + else if (in_array('country', $r['types'])) + $aRet[$sPrefix . 'country'] = $r['short_name']; + } + } + + return $aRet; + } + + /** + * Add location for the content from POST data + * @param $iId content id + * @param $sPrefix field prefix for POST data, or empty - if no prefix + * @param $oForm form to use to get POST data, or null - then new form instance will be created + */ + public function locationsAddFromForm($iId, $sPrefix = '', $oForm = null) + { + if (!$this->locationsIsEnabled()) + return; + + call_user_func_array(array($this, 'locationsAdd'), array_merge(array($iId), self::locationsRetrieveFromForm($sPrefix, $oForm))); + } + + /** + * Get locations string with links + * @param $iId content id + * @return string with links to country and city + */ + public function locationsString($iId, $bHTML = true, $aParams = array()) + { + bx_import('BxDolForm'); + $aLocation = $this->locationGet($iId); + return $this->locationsStringFromArray($aLocation, $bHTML, $aParams); + } + + /** + * Get locations string with links + * @param $aLocation location array + * @return string with links to country and city + */ + public function locationsStringFromArray($aLocation, $bHTML = true, $aParams = array()) + { + $s = ''; + $aCountries = BxDolFormQuery::getDataItems('Country'); + + if(!$aLocation || !$aLocation['country'] || !isset($aCountries[$aLocation['country']])) { + if (!empty($aLocation['lat']) || !empty($aLocation['lng'])) + $s = _t('_sys_location_country', $aLocation['lat'] . ', ' . $aLocation['lng']); + else + return ''; + } + + if (!$s) { + $sCountryUrl = '' . $aCountries[$aLocation['country']] . ''; + + if(empty($aLocation['state']) || empty($aLocation['city']) || (isset($aParams['country_only']) && $aParams['country_only'])) + $s = _t('_sys_location_country', $sCountryUrl); + } + + if (!$s) { + $sCityUrl = '' . bx_process_output($aLocation['city']) . ''; + $sStateUrl = '' . bx_process_output($aLocation['state']) . ''; + + if (empty($aLocation['street']) || empty($aLocation['street_number']) || (isset($aParams['country_city_only']) && $aParams['country_city_only'])) + $s = _t('_sys_location_country_city', $sCountryUrl, $sStateUrl, $sCityUrl); + elseif (!isset($aParams['country_city_only']) || !$aParams['country_city_only']) + $s = _t('_sys_location_country_city_street', $sCountryUrl, $sStateUrl, $sCityUrl, bx_process_output($aLocation['street']), bx_process_output($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 = false, $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; + } + + $aIndexes = ['country' => 'sCountry', 'state' => 'sState', 'city' => 'sCity', 'zip' => 'sZip']; + $aOperators = ['city' => 'like']; + + foreach ($aIndexes as $sIndex => $sVar) { + if (!$$sVar) + continue; + + $oSearchResult->aCurrent['restriction']['meta_location_' . $sIndex] = [ + 'value' => $$sVar, + 'field' => $sIndex, + 'operator' => isset($aOperators[$sIndex]) ? $aOperators[$sIndex] : '=', + 'table' => $this->_aObject['table_locations'], + ]; + } + + $oSearchResult->aCurrent['join']['meta_location'] = array( + 'type' => 'INNER', + 'table' => $this->_aObject['table_locations'], + 'mainField' => $oSearchResult->aCurrent['ident'], + 'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'], + 'onField' => 'object_id', + 'joinFields' => array(), + ); + } + + /** + * Get part of SQL query for meta locations + * @param $sContentTable content table or alias + * @param $sContentField content table field or field alias + * @param $sCountry country and other location info + */ + public function locationsGetAsSQLPart($sContentTable, $sContentField, $sCountry = false, $sState = false, $sCity = false, $sZip = false, $aBounds = array()) + { + if (empty($this->_aObject['table_locations'])) + return array(); + + return call_user_func_array(array($this->_oQuery, 'locationsGetSQLParts'), func_get_args()); + } + + /** + * Get location + * @param $iId content id + * @param $sPrefix field prefix for returning data array + * @return location array with the following keys (when no prefix specified): object_id, lat, lng, country, state, city, zip, street, street_number + */ + public function locationGet($iId, $sPrefix = '') + { + if (!$this->locationsIsEnabled()) + return; + + $a = $this->_oQuery->locationGet($iId); + if (!$sPrefix) + return $a; + + $aRet = array(); + foreach ($a as $sKey => $sVal) + $aRet[$sPrefix . '_' . $sKey] = $sVal; + return $aRet; + } + + /** + * Checks if mentions enabled for current metatags object + */ + public function mentionsIsEnabled() + { + return empty($this->_aObject['table_mentions']) ? false : true; + } + + /** + * Add \@mentions from the string (most probably \@mentions will be some sort of links already, so parsing may have to look for smth like mention name instead of \@mention, since there is no usernames for profiles modules and name could contain spaces and othr characters) + * @param $iId content id + * @param $s string with \@mentions + * @return number of found mentions + */ + public function mentionsAdd($iId, $s) + { + return $this->_metaAdd($iId, $s, '/data\-profile\-id="([0-9a-zA-Z]+)"/u', 'mentionsDelete', 'mentionsAdd', 'mentionsGet', (int)getParam('sys_metatags_mentions_max'), 'mention'); + } + + /** + * Add \@mentions from the content of form fields + * @param $iId content id + * @param $aContentInfo content info array + * @param $CNF module config array + * @param $sFormDisplay form display object + * @return number of found keywords + */ + public function mentionsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) + { + $aFields = $this->metaFields($aContentInfo, $CNF, $sFormDisplay, true); + $sTextWithKeywords = ''; + foreach ($aFields as $sField) + $sTextWithKeywords .= "\n" . $aContentInfo[$sField]; + + return $this->mentionsAdd($iId, $sTextWithKeywords); + } + + /** + * Add links to the \@mentions in the string (actual tranformation may have to be performed with ready links like mention name) + * @param $iId content id + * @param $s string with \@mentions + * @return modified string where all \@mentions are transformed to links + */ + public function mentionsParse($iId, $s) + { + if (!bx_is_api()) + return $s; + + if(($sRootUrl = getParam('sys_api_url_root_email')) !== '') { + if(substr(BX_DOL_URL_ROOT, -1) == '/' && substr($sRootUrl, -1) != '/') + $sRootUrl .= '/'; + + $s = str_replace(BX_DOL_URL_ROOT, $sRootUrl, $s); + } + + return $s; + } + + /** + * No mentions meta info in the head section + */ + protected function mentionsAddMeta($iId) + { + return 0; + } + + /** + * Get list of profile IDs associated with the content + * @return array of profile IDs + */ + public function mentionsGet($iId) + { + return $this->_oQuery->mentionsGet($iId); + } + + /** + * Set condition for search results object for mentions + * @param $oSearchResult search results object + * @param $sMention smbd + */ + public function mentionsSetSearchCondition($oSearchResult, $iProfileId, $iCmtsSystemId = 0) + { + if (!$this->mentionsIsEnabled()) + return; + + if ('sys_cmts' == $oSearchResult->aCurrent['object_metatags']) { + $this->mentionsSetSearchConditionCmts($oSearchResult, $iProfileId, $iCmtsSystemId); + return; + } + + $oSearchResult->aCurrent['restriction']['meta_mentions'] = array( + 'value' => $iProfileId, + 'field' => 'profile_id', + 'operator' => '=', + 'table' => $this->_aObject['table_mentions'], + ); + + $oSearchResult->aCurrent['join']['meta_mentions'] = array( + 'type' => 'INNER', + 'table' => $this->_aObject['table_mentions'], + 'mainField' => $oSearchResult->aCurrent['ident'], + 'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'], + 'onField' => 'object_id', + 'joinFields' => array(), + ); + } + + public function mentionsSetSearchConditionCmts($oSearchResult, $iProfileId, $iCmtsSystemId = 0) + { + if (!$this->keywordsIsEnabled()) + return; + + if ($iCmtsSystemId) { + $oSearchResult->aCurrent['restriction']['meta_mentions_cmts_system'] = array( + 'value' => $iCmtsSystemId, + 'field' => 'system_id', + 'operator' => '=', + 'table' => 'sys_cmts_ids', + ); + } + + $oSearchResult->aCurrent['restriction']['meta_mentions'] = array( + 'value' => $iProfileId, + 'field' => 'profile_id', + 'operator' => '=', + 'table' => $this->_aObject['table_mentions'], + ); + + $oSearchResult->aCurrent['join']['meta_mentions_cmts'] = array( + 'type' => 'INNER', + 'table' => 'sys_cmts_ids', + 'mainField' => $oSearchResult->aCurrent['ident'], + 'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'], + 'onField' => 'cmt_id', + 'joinFields' => array(), + ); + + $oSearchResult->aCurrent['join']['meta_mentions'] = array( + 'type' => 'INNER', + 'table' => $this->_aObject['table_mentions'], + 'mainField' => 'id', + 'mainTable' => 'sys_cmts_ids', + 'onField' => 'object_id', + 'joinFields' => array(), + ); + } + + /** + * Delete all data associated with the content + * @param $iId content id + */ + public function onDeleteContent($iId) + { + $i = 0; + foreach ($this->_aMetas as $sMeta) { + $sFunc = $sMeta . 'Delete'; + $i += $this->_oQuery->$sFunc($iId); + } + return $i; + } + + protected function _metaAdd($iId, $s, $sPreg, $sFuncDelete, $sFuncAdd, $sFuncGet, $iMaxItems, $sAlertName) + { + $a = array(); + if (!preg_match_all($sPreg, $s, $a)) { + $this->_oQuery->$sFuncDelete($iId); + return 0; + } + + $aMetas = array_unique($a[1]); + $aMetas = array_slice($aMetas, 0, $iMaxItems); + + // check if keywords/mentions were changed + $aMetasOld = $this->$sFuncGet($iId); + if (is_array($aMetas) && is_array($aMetasOld) + && count($aMetas) == count($aMetasOld) + && empty(array_diff($aMetas, $aMetasOld)) + && empty(array_diff($aMetasOld, $aMetas)) + ) { + return 0; + } + + if ($iRet = $this->_oQuery->$sFuncAdd($iId, $aMetas)) { + foreach ($aMetas as $sMeta) { + $iObjectId = 'mention' == $sAlertName ? $sMeta : $iId; + + /** + * @hooks + * @hookdef hook-meta_keyword-before_added 'meta_keyword', 'before_added' - hook to override meta keyword before it will be processed + * - $unit_name - equals `meta_keyword` + * - $action - equals `before_added` + * - $object_id - object id + * - $sender_id - currently logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `meta` - [string] by ref, keyword, can be overridden in hook processing + * - `content_id` - [int] content id + * - `object` - [string] metatags object name + * @hook @ref hook-meta_keyword-before_added + */ + /** + * @hooks + * @hookdef hook-meta_mention-before_added 'meta_mention', 'before_added' - hook to override meta mention before it will be processed + * It's equivalent to @ref hook-meta_keyword-before_added + * except mention value is used in $object_id + * @hook @ref hook-meta_mention-before_added + */ + bx_alert('meta_' . $sAlertName, 'before_added', $iObjectId, bx_get_logged_profile_id(), [ + 'meta' => &$sMeta, + 'content_id' => $iId, + 'object' => $this->_sObject + ]); + + $sSource = $this->_sObject . '_' . $iId; + /** + * @hooks + * @hookdef hook-bx_dol_metatags-keyword_added '{object_name}', 'keyword_added' - hook after meta keyword was processed (added) + * - $unit_name - metatags object name + * - $action - equals `keyword_added` + * - $object_id - object id + * - $sender_id - currently logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `meta` - [string] keyword + * - `content_id` - [int] content id + * - `source` - [string] unique source id + * @hook @ref hook-bx_dol_metatags-keyword_added + */ + /** + * @hooks + * @hookdef hook-bx_dol_metatags-mention_added '{object_name}', 'mention_added' - hook after meta mention was processed + * It's equivalent to @ref hook-bx_dol_metatags-keyword_added + * @hook @ref hook-bx_dol_metatags-mention_added + */ + bx_alert($this->_sObject, $sAlertName . '_added', $iObjectId, bx_get_logged_profile_id(), [ + 'meta' => $sMeta, + 'content_id' => $iId, + 'source' => $sSource + ]); + + /** + * @hooks + * @hookdef hook-meta_keyword-added 'meta_keyword', 'added' - hook after meta keyword was processed (added) + * - $unit_name - equals `meta_keyword` + * - $action - equals `added` + * - $object_id - object id + * - $sender_id - currently logged in profile id + * - $extra_params - array of additional params with the following array keys: + * - `meta` - [string] keyword + * - `content_id` - [int] content id + * - `source` - [string] unique source id + * - `object` - [string] metatags object name + * @hook @ref hook-meta_keyword-added + */ + /** + * @hooks + * @hookdef hook-meta_mention-added 'meta_mention', 'added' - hook after meta mention was processed + * It's equivalent to @ref hook-meta_keyword-added + * @hook @ref hook-meta_mention-added + */ + bx_alert('meta_' . $sAlertName, 'added', $iObjectId, bx_get_logged_profile_id(), [ + 'meta' => $sMeta, + 'content_id' => $iId, + 'source' => $sSource, + 'object' => $this->_sObject + ]); + } + } + + return $iRet; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolModuleQuery.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolModuleQuery.php new file mode 100644 index 0000000000..7439e65716 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolModuleQuery.php @@ -0,0 +1,288 @@ +prepare("SELECT * FROM `sys_modules` WHERE `id`=? LIMIT 1", $iId); + return $bFromCache ? $this->fromMemory('sys_modules_' . $iId, 'getRow', $sSql) : $this->getRow($sSql); + } + function getModuleByName($sName, $bFromCache = true) + { + $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `name`=? LIMIT 1", $sName); + return $bFromCache ? $this->fromMemory('sys_modules_' . $sName, 'getRow', $sSql) : $this->getRow($sSql); + } + function getModuleByUri($sUri, $bFromCache = true) + { + $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `uri`=? LIMIT 1", $sUri); + return $bFromCache ? $this->fromMemory('sys_modules_' . $sUri, 'getRow', $sSql) : $this->getRow($sSql); + } + function enableModuleByUri($sUri) + { + $sSql = $this->prepare("UPDATE `sys_modules` SET `enabled`='1' WHERE `uri`=? LIMIT 1", $sUri); + return (int)$this->query($sSql) > 0; + } + function disableModuleByUri($sUri) + { + $sSql = $this->prepare("UPDATE `sys_modules` SET `enabled`='0' WHERE `uri`=? LIMIT 1", $sUri); + return (int)$this->query($sSql) > 0; + } + function setModulePendingUninstall($sUri, $bPendingUninstall) + { + $sSql = $this->prepare("UPDATE `sys_modules` SET `pending_uninstall` = ? WHERE `uri` = ? LIMIT 1", $bPendingUninstall ? 1 : 0, $sUri); + return $this->query($sSql); + } + function isModule($sUri) + { + $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `uri`=? LIMIT 1", $sUri); + return (int)$this->fromMemory('sys_module_' . $sUri, 'getOne', $sSql) > 0; + } + function isModuleByName($sName) + { + $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `name`=? LIMIT 1", $sName); + return (int)$this->fromMemory('sys_module_' . $sName, 'getOne', $sSql) > 0; + } + function isModuleParamsUsed($sName, $sUri, $sPath, $sPrefixDb, $sPrefixClass) + { + $sSql = "SELECT `id` FROM `sys_modules` WHERE `name`=:name || `uri`=:uri || `path`=:path || `db_prefix`=:db_prefix || `class_prefix`=:class_prefix LIMIT 1"; + return (int)$this->getOne($sSql, [ + 'name' => $sName, + 'uri' => $sUri, + 'path' => $sPath, + 'db_prefix' => $sPrefixDb, + 'class_prefix' => $sPrefixClass + ]) > 0; + } + function isEnabled($sUri) + { + $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `uri`=? AND `enabled`='1' LIMIT 1", $sUri); + return (int)$this->fromMemory('sys_module_enabled_' . $sUri, 'getOne', $sSql) > 0; + } + function isEnabledByName($sName) + { + $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `name`=? AND `enabled`='1' LIMIT 1", $sName); + return (int)$this->fromMemory('sys_module_enabled_' . $sName, 'getOne', $sSql) > 0; + } + function getModules() + { + $sSql = "SELECT * FROM `sys_modules` ORDER BY `title`"; + return $this->fromMemory('sys_modules', 'getAll', $sSql); + } + function getModulesBy($aParams = array(), $bFromCache = true) + { + $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']]; + $sPostfix = $sWhereClause = $sOrderByClause = ""; + $sSelectClause = "`id`, `type`, `name`, `title`, `vendor`, `version`, `help_url`, `path`, `uri`, `class_prefix`, `db_prefix`, `lang_category`, `date`, `enabled`"; + $aBindings = []; + + switch($aParams['type']) { + case 'type': + if(!is_array($aParams['value'])) + $aParams['value'] = array($aParams['value']); + + $sPostfix .= '_type_' . implode('_', $aParams['value']); + $sWhereClause .= " AND `type` IN (" . $this->implode_escape($aParams['value']) . ")"; + break; + + case 'modules': + $sPostfix .= '_modules'; + $aBindings['type'] = BX_DOL_MODULE_TYPE_MODULE; + + $sWhereClause .= " AND `type`=:type"; + break; + + case 'modules_subtypes': + $sPostfix .= '_modules_subtypes_'; + + if(isset($aParams['id_as_key']) && $aParams['id_as_key'] === true) { + $sPostfix .= 'id_as_key_'; + + $aMethod['name'] = 'getAllWithKey'; + $aMethod['params'][1] = 'id'; + } + + if(isset($aParams['name_as_key']) && $aParams['name_as_key'] === true) { + $sPostfix .= 'name_as_key_'; + + $aMethod['name'] = 'getAllWithKey'; + $aMethod['params'][1] = 'name'; + } + + if(!is_array($aParams['value'])) + $aParams['value'] = [$aParams['value']]; + + $sPostfix .= implode('_', $aParams['value']); + $aBindings['type'] = BX_DOL_MODULE_TYPE_MODULE; + + $iSubtypes = 0; + foreach($aParams['value'] as $iSubtype) + $iSubtypes |= pow(2, $iSubtype); + + $aBindings['subtypes'] = $iSubtypes; + + $sWhereClause .= " AND `type`=:type AND `subtypes` & :subtypes"; + break; + + case 'is_module_subtype': + $sPostfix .= '_is_module_' . $aParams['subtype'] . '_' . $aParams['name']; + $aMethod['name'] = 'getOne'; + $aBindings = [ + 'type' => BX_DOL_MODULE_TYPE_MODULE, + 'subtypes' => pow(2, (int)$aParams['subtype']), + 'name' => $aParams['name'] + ]; + $sSelectClause = "`id`"; + $sWhereClause .= " AND `type`=:type AND `subtypes` & :subtypes AND `name`=:name"; + break; + + case 'languages': + $sPostfix .= '_languages'; + $aBindings['type'] = BX_DOL_MODULE_TYPE_LANGUAGE; + + $sWhereClause .= " AND `type`=:type"; + break; + + case 'templates': + $sPostfix .= '_templates'; + $aBindings['type'] = BX_DOL_MODULE_TYPE_TEMPLATE; + + $sWhereClause .= " AND `type`=:type"; + break; + + case 'path_and_uri': + $sPostfix .= '_path_and_uri_' . $aParams['path'] . '_' . $aParams['uri']; + $aMethod['name'] = 'getRow'; + $aBindings = array_merge($aBindings, array( + 'path' => $aParams['path'], + 'uri' => $aParams['uri'] + )); + + $sWhereClause .= " AND `path`=:path AND `uri`=:uri"; + break; + + case 'all_pairs_name_uri': + $sPostfix .= 'all_pairs_name_uri'; + $aMethod['name'] = 'getPairs'; + $aMethod['params'][1] = 'name'; + $aMethod['params'][2] = 'uri'; + break; + + case 'all_key_name': + $sPostfix .= 'all_key_name'; + $aMethod['name'] = 'getAllWithKey'; + $aMethod['params'][1] = 'name'; + break; + + case 'all': + break; + } + + if(isset($aParams['active'])) { + $sPostfix .= "_active"; + $aBindings['enabled'] = (int)$aParams['active']; + + $sWhereClause .= " AND `enabled`=:enabled"; + } + + $sOrderByClause = " ORDER BY " . (isset($aParams['order_by']) ? $aParams['order_by'] : '`title`'); + + $aMethod['params'][0] = "SELECT " . $sSelectClause . " + FROM `sys_modules` + WHERE 1 " . $sWhereClause . $sOrderByClause; + $aMethod['params'][] = $aBindings; + + if(!$bFromCache || empty($sPostfix)) + return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']); + + return call_user_func_array(array($this, 'fromMemory'), array_merge(array('sys_modules' . $sPostfix, $aMethod['name']), $aMethod['params'])); + } + + function isModuleContent($sName) + { + return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_TEXT, 'name' => $sName]) != 0; + } + + function isModuleContext($sName) + { + return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_CONTEXT, 'name' => $sName]) != 0; + } + + function isModuleProfile($sName) + { + return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_PROFILE, 'name' => $sName]) != 0; + } + + function getModulesUri() + { + return $this->fromMemory('sys_modules_uri', 'getColumn', 'SELECT `uri` FROM `sys_modules` ORDER BY `uri`'); + } + + function getDependent($sName, $sUri) + { + $aResults = array(); + + $aModules = $this->getAll("SELECT `id`, `title`, `dependencies`, `enabled` FROM `sys_modules` WHERE (`dependencies` LIKE " . $this->escape('%' . $sName . '%') . " OR `dependencies` LIKE " . $this->escape('%' . $sUri . '%') . ") AND `enabled`='1'"); + foreach($aModules as $aModule) { + $aDependencies = explode(',', $aModule['dependencies']); + if(in_array($sName, $aDependencies) || in_array($sUri, $aDependencies)) + $aResults[] = $aModule; + } + + return $aResults; + } + + public function updateModule($aParamsSet, $aParamsWhere = array()) + { + if(empty($aParamsSet)) + return false; + + $sWhereClause = !empty($aParamsWhere) ? $this->arrayToSQL($aParamsWhere, " AND ") : "1"; + + $sSql = "UPDATE `sys_modules` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $sWhereClause; + return $this->query($sSql); + } + + public function checkModulesSubtypes() + { + return (int)$this->getOne("SELECT COUNT(`id`) FROM `sys_modules` WHERE `type` = :type AND `subtypes` <> 0", [ + 'type' => BX_DOL_MODULE_TYPE_MODULE + ]) > 0; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfile.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfile.php new file mode 100644 index 0000000000..a5248800ae --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfile.php @@ -0,0 +1,821 @@ +_iProfileID = $iProfileId; // since constructor is protected $iProfileId is always valid + $this->_oQuery = BxDolProfileQuery::getInstance(); + } + + /** + * Prevent cloning the instance + */ + public function __clone() + { + $sClass = get_class($this) . '_' . $this->_iProfileID; + if (isset($GLOBALS['bxDolClasses'][$sClass])) + trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); + } + + /** + * Get singleton instance of Account Profile by account id + */ + public static function getInstanceAccountProfile($iAccountId = false, $bClearCache = false) + { + if (!$iAccountId) + $iAccountId = getLoggedId(); + $oQuery = BxDolProfileQuery::getInstance(); + $aProfile = $oQuery->getProfileByContentTypeAccount($iAccountId, 'system', $iAccountId); + if (!$aProfile) + return false; + return self::getInstance($aProfile['id'], $bClearCache); + } + + /** + * Get singleton instance of Profile by account id, content id and type + */ + public static function getInstanceByContentTypeAccount($iContent, $sType, $iAccountId = false) + { + if (!$iAccountId) + $iAccountId = getLoggedId(); + $oQuery = BxDolProfileQuery::getInstance(); + $aProfile = $oQuery->getProfileByContentTypeAccount($iContent, $sType, $iAccountId); + if (!$aProfile) + return false; + return self::getInstance($aProfile['id']); + } + + /** + * Get singleton instance of Profile by content id and type + */ + public static function getInstanceByContentAndType($iContent, $sType, $bClearCache = false) + { + $oQuery = BxDolProfileQuery::getInstance(); + $aProfile = $oQuery->getProfileByContentAndType($iContent, $sType, $bClearCache); + if (!$aProfile) + return false; + return self::getInstance($aProfile['id']); + } + + /** + * Get singleton instance of Profile by Account id (currently active profile is returned) + */ + public static function getInstanceByAccount($iAccountId = false, $bClearCache = false) + { + $oQuery = BxDolProfileQuery::getInstance(); + $mixedProfileId = $oQuery->getCurrentProfileByAccount($iAccountId, $bClearCache); + + return self::getInstance($mixedProfileId); + } + + /** + * Get singleton instance of Profile by profile id, if profile isn't found it returns instance of BxDolProfileAnonymous or BxDolProfileUndefined + */ + public static function getInstanceMagic($mixedProfileId = false, $bClearCache = false) + { + if ($mixedProfileId < 0) + return BxDolProfileAnonymous::getInstance($mixedProfileId); + + if (0 === $mixedProfileId || !($oProfile = self::getInstance($mixedProfileId, $bClearCache))) + return BxDolProfileUndefined::getInstance(); + + return $oProfile; + } + + /** + * Get singleton instance of Profile by profile id + */ + public static function getInstance($mixedProfileId = false, $bClearCache = false) + { + $oQuery = BxDolProfileQuery::getInstance(); + + if (!$mixedProfileId) + $mixedProfileId = $oQuery->getCurrentProfileByAccount(getLoggedId(), $bClearCache); + + $aProfileInfo = $oQuery->getInfoById($mixedProfileId); + if (empty($aProfileInfo['id']) || !BxDolModuleDb::getInstance()->isEnabledByName($aProfileInfo['type'])) + return false; + + $sClass = __CLASS__ . '_' . $aProfileInfo['id']; + if (!isset($GLOBALS['bxDolClasses'][$sClass])) + $GLOBALS['bxDolClasses'][$sClass] = new BxDolProfile($aProfileInfo['id']); + + return $GLOBALS['bxDolClasses'][$sClass]; + } + + public static function getData($mixedProfileId = false, $aParams = []) + { + $sDisplayType = 'unit'; + if(isset($aParams['display_type'])) + $sDisplayType = $aParams['display_type']; + + if(!($mixedProfileId instanceof BxDolProfile)) + $oProfile = BxDolProfile::getInstanceMagic($mixedProfileId); + else + $oProfile = $mixedProfileId; + + $aRv = [ + 'id' => $oProfile->id(), + 'display_type' => $sDisplayType, + 'display_name' => $oProfile->getDisplayName(), + 'url' => bx_api_get_relative_url($oProfile->getUrl()), + 'url_avatar' => $oProfile->{isset($aParams['get_avatar']) && method_exists($oProfile, $aParams['get_avatar']) ? $aParams['get_avatar'] : 'getAvatar'}(), + 'module' => $oProfile->getModule(), + ]; + + if(isset($aParams['with_info']) && (bool)$aParams['with_info']) + $aRv['info'] = bx_srv($oProfile->getModule(), 'get_info', [$oProfile->getContentId(), false]); + + return $aRv; + } + + public static function getDataForPage($mixedProfileId = false, $aParams = []) + { + if(!($mixedProfileId instanceof BxDolProfile)) + $oProfile = BxDolProfile::getInstanceMagic($mixedProfileId); + else + $oProfile = $mixedProfileId; + + $iId = $oProfile->id(); + $oAccount = BxDolAccount::getInstance(getLoggedId()); + + $aMembershipInfo = BxDolAcl::getInstance()->getMemberMembershipInfo($iId); + + $aRv = [ + 'id' => $iId, + 'account_id' => $oAccount->id(), + 'email' => $oAccount->getEmail(), + 'display_name' => $oProfile->getDisplayName(), + 'url' => bx_api_get_relative_url($oProfile->getUrl()), + 'avatar' => $oProfile->getAvatar(), + 'settings' => $oProfile->getSettings(), + 'membership' => $aMembershipInfo['id'], + //'level' => BxDolAcl::getInstance()->getMemberMembershipInfo($iId), + 'moderator' => (bool)BxDolAcl::getInstance()->isMemberLevelInSet([MEMBERSHIP_ID_ADMINISTRATOR, MEMBERSHIP_ID_MODERATOR], $iId), + 'operator' => isAdmin(), + //'info' => $oProfile->getInfo(), + 'confirmed' => $oAccount->isConfirmed(), + 'notifications' => 0, + 'cart' => 0, + 'profiles_count' => $oAccount->getProfilesNumber(true), + 'hash' => encryptUserId($iId), + 'profiles_limit' => (int)getParam('sys_account_limit_profiles_number'), + 'active' => $oProfile->isActive(), + 'status' => $oProfile->getStatus(), + ]; + + if ($iId == bx_get_logged_profile_id()){ + $oInformer = BxDolInformer::getInstance(BxDolTemplate::getInstance()); + $sRet = $oInformer ? $oInformer->display() : ''; + if ($sRet){ + $aRv['informer'] = $sRet; + } + + $oPayments = BxDolPayments::getInstance(); + if($oPayments->isActive()) + $aRv['cart'] = $oPayments->getCartItemsCount(); + + $sModuleNotifications = 'bx_notifications'; + if(BxDolRequest::serviceExists($sModuleNotifications, 'get_unread_notifications_num')) + $aRv['notifications'] = bx_srv($sModuleNotifications, 'get_unread_notifications_num', [$iId]); + + if($o !== false && BxDolAccount::isAllowedCreateMultiple($iId)) { + $oAccount = BxDolAccount::getInstance(); + if($oAccount != false && !$oAccount->isProfilesLimitReached()) { + $oMenuProfileAdd = BxDolMenu::getObjectInstance('sys_add_profile'); + if($oMenuProfileAdd !== false) + $aRv['menu'] = $oMenuProfileAdd->getCodeAPI(); + } + } + } + + + return $aRv; + } + + /** + * Get profile id + */ + public function id() + { + $aProfile = $this->getInfo($this->_iProfileID); + return isset($aProfile['id']) ? $aProfile['id'] : false; + } + + /** + * Get account id associated with the profile + */ + public function getAccountId($iProfileId = false) + { + $aInfo = $this->getInfo($iProfileId); + return $aInfo['account_id']; + } + + /** + * Get account object associated with the profile + */ + public function getAccountObject($iProfileId = false) + { + return BxDolAccount::getInstance($this->getAccountId($iProfileId)); + } + + /** + * Get content id associated with the profile + */ + public function getContentId() + { + $aInfo = $this->getInfo(); + return $aInfo['content_id']; + } + + /** + * Check if profile status is active + */ + public function isActive($iProfileId = false) + { + if($this->getStatus($iProfileId) != BX_PROFILE_STATUS_ACTIVE) + return false; + + return (($oAccount = $this->getAccountObject($iProfileId)) !== false && $oAccount->isConfirmed()) || !getParam('sys_account_hide_unconfirmed_accounts'); + } + + /** + * Is profile online + */ + public function isOnline($iProfileId = false) + { + $iProfileId = (int)$iProfileId ? $iProfileId : $this->_iProfileID; + return $this->_oQuery->isOnline($iProfileId); + } + + /** + * Is profile can 'Act as Profile' + */ + public function isActAsProfile($iProfileId = false) + { + $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID); + return BxDolService::call($aInfo['type'], 'act_as_profile'); + } + + /** + * Get profile status + */ + public function getStatus($iProfileId = false) + { + $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID); + if(empty($aInfo) || !is_array($aInfo)) + return false; + + return $aInfo['status']; + } + + /** + * Get profile module name + */ + public function getModule($iProfileId = false) + { + $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID); + return $aInfo['type']; + } + + /** + * Get profile info + */ + public function getInfo($iProfileId = 0) + { + if ($iProfileId && $iProfileId != $this->_iProfileID) + return $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID); + + if ($this->_aProfile) + return $this->_aProfile; + + $this->_aProfile = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID); + return $this->_aProfile; + } + + /** + * Validate profile id. + * @param $s - profile id + * @return profile id or false if profile was not found + */ + static public function getID($s) + { + $iId = BxDolProfileQuery::getInstance()->getIdById((int)$s); + return $iId ? $iId : false; + } + + /** + * Get name to display in thumbnail + */ + public function getDisplayName($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + $sDisplayName = BxDolService::call($aInfo['type'], 'profile_name', array($aInfo['content_id'])); + /** + * @hooks + * @hookdef hook-profile-profile_name 'profile', 'profile_name' - hook on before profile deletion + * - $unit_name - equals `profile` + * - $action - equals `profile_name` + * - $object_id - profile_id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `info` - [array] profile info + * - `display_name` - [bool] by ref, display profile name, can be overridden in hook processing + * @hook @ref hook-profile-profile_name + */ + bx_alert('profile', 'profile_name', $iProfileId, 0, array('info' => $aInfo, 'display_name' => &$sDisplayName)); + return $sDisplayName; + } + + public function getSettings($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_settings', array($aInfo['content_id'])); + } + + /** + * Get profile url + */ + public function getUrl($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_url', array($aInfo['content_id'])); + } + + /** + * Get profile unit + */ + public function getUnit($iProfileId = 0, $aParams = array()) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_unit', array($aInfo['content_id'], $aParams)); + } + + /** + * Get profile unit for API calls + */ + public function getUnitAPI($iProfileId = 0, $aParams = array()) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_unit_api', array($aInfo['content_id'], $aParams)); + } + + /** + * Get badges + */ + public function getBadges($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'get_badges', array($aInfo['content_id'], false, true)); + } + + /** + * Check whether a profile has real image uploaded by user. + */ + public function hasImage($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'has_image', array($aInfo['content_id'])); + } + + /** + * Get picture url + */ + public function getPicture($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_picture', array($aInfo['content_id'])); + } + + /** + * Get avatar url + */ + public function getAvatar($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_avatar', array($aInfo['content_id'])); + } + + /** + * Get big (2x) avatar url + */ + public function getAvatarBig($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_avatar_big', array($aInfo['content_id'])); + } + + /** + * Get cover url + */ + public function getCover($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_cover', array($aInfo['content_id'])); + } + + /** + * Get unit cover url + */ + public function getUnitCover($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_unit_cover', array($aInfo['content_id'])); + } + + /** + * Get thumbnail url + */ + public function getThumb($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_thumb', array($aInfo['content_id'])); + } + + /** + * Get icon url + */ + public function getIcon($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_icon', array($aInfo['content_id'])); + } + + /** + * Get module icon + */ + public function getIconModule($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'module_icon'); + } + + /** + * get profile edit page url + */ + public function getEditUrl($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'profile_edit_url', array($aInfo['content_id'])); + } + + /** + * @see iBxDolProfile::checkAllowedProfileView + */ + public function checkAllowedProfileView($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'check_allowed_profile_view', array($aInfo['content_id'])); + } + + /** + * @see iBxDolProfile::checkAllowedProfileContact + */ + public function checkAllowedProfileContact($iProfileId = 0) + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'check_allowed_profile_contact', array($aInfo['content_id'])); + } + + + /** + * @see iBxDolProfile::checkAllowedPostInProfile + */ + public function checkAllowedPostInProfile($iProfileId = 0, $sPostModule = '') + { + $aInfo = $this->getInfo($iProfileId); + return BxDolService::call($aInfo['type'], 'check_allowed_post_in_profile', array($aInfo['content_id'], $sPostModule)); + } + + /** + * Delete profile. + * @param $ID - optional profile id to delete + * @param $bDeleteWithContent - delete profile with all its contents + * @param $bForceDelete - force deletetion is case of account profile deletion + * @return false on error, or true on success + */ + function delete($ID = false, $bDeleteWithContent = false, $bForceDelete = false) + { + $ID = (int)$ID; + if (!$ID) + $ID = $this->_iProfileID; + + $aProfileInfo = $this->_oQuery->getInfoById($ID); + if (!$aProfileInfo) + return false; + + // delete system profiles (accounts) is not allowed, instead - delete whole account + if (!$bForceDelete && 'system' == $aProfileInfo['type']) + return false; + + // delete actual profile + if ($sErrorMsg = BxDolService::call($aProfileInfo['type'] , 'delete_entity_service', array($aProfileInfo['content_id'], $bDeleteWithContent))) + return false; + + // switch profile context if deleted profile is active profile context + $oAccount = BxDolAccount::getInstance ($aProfileInfo['account_id']); + $aAccountInfo = $oAccount->getInfo(); + if (!$bForceDelete && $ID == $aAccountInfo['profile_id']) + $oAccount->updateProfileContextAuto($ID); + + $isStopDeletion = false; + /** + * @hooks + * @hookdef hook-profile-before_delete 'profile', 'before_delete' - hook on before profile deletion + * - $unit_name - equals `profile` + * - $action - equals `before_delete` + * - $object_id - profile_id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `delete_with_content` - [bool] also delete content for profile or not + * - `stop_deletion` - [bool] by ref, if true then stop profile deletion, can be overridden in hook processing + * - `type` - [string] module name + * @hook @ref hook-profile-before_delete + */ + bx_alert('profile', 'before_delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion, 'type' => $aProfileInfo['type'])); + if ($isStopDeletion) + return false; + + // delete associated content + if($bDeleteWithContent) { + BxDolCmts::onAuthorDelete($ID); + + BxDolReport::onAuthorDelete($ID); + + BxDolVote::onAuthorDelete($ID); + + BxDolFavorite::onAuthorDelete($ID); + + BxDolView::onAuthorDelete($ID); + } + + // delete connections + $oConn = BxDolConnection::getObjectInstance('sys_profiles_friends'); + $oConn->onDeleteInitiatorAndContent($ID); + + $oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions'); + $oConn->onDeleteInitiatorAndContent($ID); + + // delete profile's acl levels + BxDolAcl::getInstance()->onProfileDelete($ID); + + // delete SEO links + BxDolPage::deleteSeoLinkByParam ('profile_id', $ID); + + // delete profile + if (!$this->_oQuery->delete($ID)) + return false; + + /** + * @hooks + * @hookdef hook-profile-delete 'profile', 'delete' - hook on profile deleted + * - $unit_name - equals `profile` + * - $action - equals `delete` + * - $object_id - profile_id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `delete_with_content` - [bool] also delete content for profile or not + * - `type` - [string] module name + * @hook @ref hook-profile-delete + */ + bx_alert('profile', 'delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'type' => $aProfileInfo['type'])); + + // unset class instance to prevent creating the instance again + $this->_iProfileID = 0; + $sClass = get_class($this) . '_' . $ID; + unset($GLOBALS['bxDolClasses'][$sClass]); + + return true; + } + + /** + * Insert account and content id association. Also if currect profile id is not defined - it updates current profile id in account. + * @param $iAccountId account id + * @param $iContentId content id + * @param $sStatus profile status + * @param $sType profile content type + * @return inserted profile's id + */ + static public function add ($iAction, $iAccountId, $iContentId, $sStatus, $sType = 'system') + { + $oQuery = BxDolProfileQuery::getInstance(); + if (!($iProfileId = $oQuery->insertProfile ($iAccountId, $iContentId, $sStatus, $sType))) + return false; + /** + * @hooks + * @hookdef hook-profile-add 'profile', 'add' - hook on profile added + * - $unit_name - equals `profile` + * - $action - equals `add` + * - $object_id - profile_id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `module` - [string] action id + * - `content` - [int] content_id in module + * - `account` - [int] account_id + * - `status` - [string] status + * - `action` - [int] action id + * - `profile_id` - [int] by ref, iprofile_id, can be overridden in hook processing + * @hook @ref hook-profile-add + */ + bx_alert('profile', 'add', $iProfileId, 0, array('module' => $sType, 'content' => $iContentId, 'account' => $iAccountId, 'status' => $sStatus, 'action' => $iAction, 'profile_id' => &$iProfileId)); + return $iProfileId; + } + + /** + * Change profile status to 'Active' + */ + public function activate($iAction, $iProfileId = 0, $bSendEmailNotification = true) + { + $sStatus = $this->getStatus($iProfileId); + return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, BX_PROFILE_STATUS_PENDING == $sStatus ? 'approve' : 'activate', $iAction, $iProfileId, $bSendEmailNotification); + } + + /** + * Change profile status from 'Pending' to the next level - 'Active' + */ + public function approve($iAction, $iProfileId = 0, $bSendEmailNotification = true) + { + return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, 'approve', $iAction, $iProfileId, $bSendEmailNotification); + } + + /** + * Change profile status to 'Pending' + */ + public function disapprove($iAction, $iProfileId = 0, $bSendEmailNotification = true) + { + return $this->changeStatus(BX_PROFILE_STATUS_PENDING, 'disapprove', $iAction, $iProfileId, $bSendEmailNotification); + } + + /** + * Move profile to another account + */ + public function move($iAccountId, $iProfileId = 0) + { + if (!$iProfileId) + $iProfileId = $this->_iProfileID; + + if($this->getAccountId($iProfileId) == $iAccountId) + return true; + + return $this->_oQuery->changeAccountId($iProfileId, $iAccountId) !== false; + } + + public function doAudit($sAction, $aData = array()) + { + bx_audit( + $this->getContentId(), + $this->getModule(), + $sAction, + array('content_title' => $this->getDisplayName(), 'data' => $aData) + ); + } + + /** + * Change profile status to 'Suspended' + */ + public function suspend($iAction, $iProfileId = 0, $bSendEmailNotification = true) + { + if (!$iProfileId) + $iProfileId = $this->_iProfileID; + + //moderators shouldn't be able to suspend other moderators and admins + if (BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR), bx_get_logged_profile_id()) && BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR), $iProfileId)) + return false; + + return $this->changeStatus(BX_PROFILE_STATUS_SUSPENDED, 'suspend', $iAction, $iProfileId, $bSendEmailNotification); + } + + protected function changeStatus($sStatus, $sAlertActionName, $iAction, $iProfileId = 0, $bSendEmailNotification = true) + { + if (!$iProfileId) + $iProfileId = $this->_iProfileID; + + // get account and profile objects + $oProfile = BxDolProfile::getInstance($iProfileId); + $oAccount = $oProfile->getAccountObject(); + if (!$oProfile || !$oAccount) + return false; + + // change status + if (!$this->_oQuery->changeStatus($iProfileId, $sStatus)) + return false; + + $this->_aProfile = array(); + + /** + * @hooks + * @hookdef hook-profile-approve 'profile', 'approve' - hook on switch profile + * - $unit_name - equals `profile` + * - $action - equals `approve` + * - $object_id - profile_id for current user + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `action` - [int] action id + * - `status` - [string] by ref, status, can be overridden in hook processing + * - `send_email_notification` - [bool] by ref, if need to send notification about changed status = true, otherwise false, can be overridden in hook processing + * @hook @ref hook-profile-approve + */ + bx_alert('profile', $sAlertActionName, $iProfileId, false, array('action' => $iAction, 'status' => &$sStatus, 'send_email_notification' => &$bSendEmailNotification)); + + $this->doAudit('_sys_audit_action_set_status_' . $sStatus); + + // send email to member about status change + if ($bSendEmailNotification) + sendMailTemplate('t_ChangeStatus' . ucfirst($sStatus), $oAccount->id(), $iProfileId, array('status' => $sStatus), BX_EMAIL_SYSTEM); + + return true; + } + + public static function getSwitchToProfileRedirectUrl($iProfileId) + { + return BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher', [ + 'switch_to_profile' => $iProfileId, + 'redirect' => getParam('sys_account_switch_to_profile_redirect') + ]); + } + + /** + * Display informer message if it is possible to switch to this profile + */ + public function checkSwitchToProfile($oTemplate = null, $iViewerAccountId = false, $iViewerProfileId = false) + { + if (false === $iViewerAccountId) + $iViewerAccountId = getLoggedId(); + if (false === $iViewerProfileId) + $iViewerProfileId = bx_get_logged_profile_id(); + + if (!$iViewerAccountId || !$iViewerProfileId) + return; + + $aCheck = checkActionModule($iViewerProfileId, 'switch to any profile', 'system', false); + $bAllowSwitchToAnyProfile = $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED; + + $iSwitchToAccountId = $this->getAccountId(); + $iSwitchToProfileId = $this->id(); + $bCanSwitch = ($iSwitchToAccountId == $iViewerAccountId || $bAllowSwitchToAnyProfile); + /** + * @hooks + * @hookdef hook-account-check_switch_context 'account', 'check_switch_context' - hook on switch profile + * - $unit_name - equals `account` + * - $action - equals `check_switch_context` + * - $object_id - account id + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `switch_to_profile` - [int] profile_id for switched profile + * - `viewer_account` - [int] profile_id for viewer profile + * - `override_result` - [bool] by ref, if allow to switch to profile = true, otherwise false, can be overridden in hook processing + * @hook @ref hook-account-check_switch_context + */ + bx_alert('account', 'check_switch_context', $iSwitchToAccountId, $iViewerProfileId, array('switch_to_profile' => $iSwitchToProfileId, 'viewer_account' => $iViewerAccountId, 'override_result' => &$bCanSwitch)); + + if(!$bCanSwitch || $iViewerProfileId == $iSwitchToProfileId) + return; + + $oInformer = BxDolInformer::getInstance($oTemplate); + if($oInformer) + $oInformer->add('sys-switch-profile-context', _t('_sys_txt_account_profile_context_change' . ($iSwitchToAccountId != $iViewerAccountId ? '_to_another' : '') . '_suggestion', self::getSwitchToProfileRedirectUrl($this->id())), BX_INFORMER_INFO); + } + + /** + * Add permament messages. + */ + public function addInformerPermanentMessages ($oInformer) + { + $aInfo = $this->getInfo(); + $aProfiles = $this->_oQuery->getProfilesByAccount($aInfo['account_id']); + + if ($aInfo['type'] == 'system' && count($aProfiles) == 1) { + $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher'); + $oInformer->add('sys-account-profile-system', _t('_sys_txt_account_profile_system', $sUrl), BX_INFORMER_ALERT); + } + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfileAnonymous.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfileAnonymous.php new file mode 100644 index 0000000000..5ff88ad15c --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolProfileAnonymous.php @@ -0,0 +1,95 @@ + $iId) + * $oProfile = BxDolProfileAnonymous::getInstance(); + * else + * $oProfile = BxDolProfile::getInstance($iId); + * + * @endcode + */ +class BxDolProfileAnonymous extends BxDolProfileUndefined +{ + protected $_oProfileOrig = null; + protected $_iProfileID = 0; + protected $_isShowRealProfile = null; + + /** + * Constructor + */ + protected function __construct ($oProfile) + { + $sClass = get_class($this) . '_' . $oProfile->id(); + if (isset($GLOBALS['bxDolClasses'][$sClass])) + trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR); + + parent::__construct(); + + $this->_iProfileID = $oProfile->id(); + $this->_oProfileOrig = $oProfile; + } + + /** + * Get singleton instance of Profile by profile id + */ + public static function getInstance($mixedProfileId = false, $bClearCache = false) + { + $oProfile = $mixedProfileId ? BxDolProfile::getInstance(abs($mixedProfileId)) : null; + if (!$oProfile) + return BxDolProfileUndefined::getInstance(); + + $sClass = __CLASS__ . '_' . $oProfile->id(); + if (!isset($GLOBALS['bxDolClasses'][$sClass])) + $GLOBALS['bxDolClasses'][$sClass] = new BxDolProfileAnonymous($oProfile); + + return $GLOBALS['bxDolClasses'][$sClass]; + } + + /** + * Get profile display name + */ + public function getDisplayName() + { + if ($this->isShowRealProfile()) + return _t('_anonymous_f', $this->_oProfileOrig->getDisplayName()); + else + return _t('_anonymous'); + } + + public function getUrl() + { + if ($this->isShowRealProfile()) + return $this->_oProfileOrig->getUrl(); + else + return 'javascript:void(0);'; + } + + public function setShowRealProfile($bValue) + { + $this->_isShowRealProfile = $bValue; + } + + protected function isShowRealProfile() + { + if (null !== $this->_isShowRealProfile) + return $this->_isShowRealProfile; + + $this->_isShowRealProfile = (isAdmin() || $this->_oProfileOrig->id() == bx_get_logged_profile_id() || BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_ADMINISTRATOR, MEMBERSHIP_ID_MODERATOR))); + + return $this->_isShowRealProfile; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolPush.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolPush.php new file mode 100644 index 0000000000..ab2bdf5d33 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolPush.php @@ -0,0 +1,221 @@ +_sAppId = getParam('sys_push_app_id'); + $this->_sRestApi = getParam('sys_push_rest_api'); + } + + /** + * Prevent cloning the instance + */ + public function __clone() + { + if (isset($GLOBALS['bxDolClasses'][get_class($this)])) + trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); + } + + /** + * Get singleton instance of the class + */ + public static function getInstance() + { + if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) + $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolPush(); + + return $GLOBALS['bxDolClasses'][__CLASS__]; + } + + /** + * Get tags to send to PUSH server + * @param $iProfileId - profile ID + * @return array of tags + */ + public static function getTags($iProfileId = false) + { + if (false === $iProfileId) + $iProfileId = bx_get_logged_profile_id(); + + $oProfile = BxDolProfile::getInstance($iProfileId); + $oAccount = $oProfile ? $oProfile->getAccountObject() : null; + if (!$oProfile || !$oAccount) + return false; + + $sEmail = $oAccount->getEmail(); + $a = array ( + 'user_hash' => encryptUserId($iProfileId), + 'real_name' => $oProfile->getDisplayName(), + 'email' => $sEmail, + 'email_hash' => $sEmail ? hash_hmac('sha256', $sEmail, getParam('sys_push_app_id')) : '', + ); + + /** + * @hooks + * @hookdef hook-system-is_confirmed 'system', 'push_tags' - hook on get tags to send to PUSH server + * - $unit_name - equals `system` + * - $action - equals `push_tags` + * - $object_id - profile_id from current user + * - $sender_id - profile_id from current user + * - $extra_params - array of additional params with the following array keys: + * - `tags` - [array] by ref, array of tags, can be overridden in hook processing + * @hook @ref hook-system-push_tags + */ + bx_alert('system', 'push_tags', $iProfileId, $iProfileId, array('tags' => &$a)); + + return $a; + } + + /** + * @param $a - array to fill with notification counter per module + * @return total number of notifications + */ + public static function getNotificationsCount($iProfileId = 0, &$aBubbles = null) + { + if ('' != trim(getParam('sys_api_url_root_push'))) { + return bx_srv('bx_notifications', 'get_unread_notifications_num', [$iProfileId]); + } + + $iMemberIdCookie = null; + $bLoggedMemberGlobals = null; + if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) { + if (getLoggedId()) + $iMemberIdCookie = getLoggedId(); + if (!empty($GLOBALS['logged']['member'])) + $bLoggedMemberGlobals = $GLOBALS['logged']['member']; + $oProfile = BxDolProfile::getInstance($iProfileId); + $_COOKIE['memberID'] = $oProfile ? $oProfile->getAccountId() : 0; + $GLOBALS['logged']['member'] = $oProfile ? true : false; + } + + $aMenusObjects = array('sys_account_notifications', 'sys_toolbar_member'); + foreach ($aMenusObjects as $sMenuObject) { + if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) + unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']); + $oMenu = BxDolMenu::getObjectInstance($sMenuObject); + if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) + unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']); + + $bSave = $oMenu->setDisplayAddons(true); + $a = $oMenu->getMenuItems(); + $iBubbles = 0; + foreach ($a as $r) { + if (!$r['bx_if:addon']['condition']) + continue; + if (null !== $aBubbles) + $aBubbles[$r['name']] = $r['bx_if:addon']['content']['addon']; + $iBubbles += $r['bx_if:addon']['content']['addon']; + } + } + + if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) { + if (null === $iMemberIdCookie) + unset($_COOKIE['memberID']); + else + $_COOKIE['memberID'] = $iMemberIdCookie; + + if (null === $bLoggedMemberGlobals) + unset($GLOBALS['logged']['member']); + else + $GLOBALS['logged']['member'] = $bLoggedMemberGlobals; + } + + return $iBubbles; + } + + public function send($iProfileId, $aMessage, $bAddToQueue = false) + { + if(empty($this->_sAppId) || empty($this->_sRestApi)) + return false; + + if($bAddToQueue && BxDolQueuePush::getInstance()->add($iProfileId, $aMessage)) + return true; + + $sUrlWeb = $sUrlApp = !empty($aMessage['url']) ? $aMessage['url'] : ''; + + if(($sRootUrl = getParam('sys_api_url_root_email')) !== '') { + if(substr(BX_DOL_URL_ROOT, -1) == '/' && substr($sRootUrl, -1) != '/') + $sRootUrl .= '/'; + + if($sUrlWeb) + $sUrlWeb = str_replace(BX_DOL_URL_ROOT, $sRootUrl, $sUrlWeb); + + if(empty($aMessage['contents']) && is_array($aMessage['contents'])) + foreach($aMessage['contents'] as $sKey => $sValue) + $aMessage['contents'][$sKey] = str_replace(BX_DOL_URL_ROOT, $sRootUrl, $sValue); + } + + if(($sRootUrl = getParam('sys_api_url_root_push')) !== '') { + if(substr(BX_DOL_URL_ROOT, -1) == '/' && substr($sRootUrl, -1) != '/') + $sRootUrl .= '/'; + + if($sUrlApp) + $sUrlApp = str_replace(BX_DOL_URL_ROOT, $sRootUrl, $sUrlApp); + } + else + $sUrlApp = $sUrlWeb; + + $aFields = [ + 'app_id' => $this->_sAppId, + 'filters' => [ + ['field' => 'tag', 'key' => 'user_hash', 'relation' => '=', 'value' => encryptUserId($iProfileId)] + ], + 'contents' => !empty($aMessage['contents']) && is_array($aMessage['contents']) ? $aMessage['contents'] : [], + 'headings' => !empty($aMessage['headings']) && is_array($aMessage['headings']) ? $aMessage['headings'] : [], + 'web_url' => $sUrlWeb, + 'app_url' => $sUrlApp, + 'data' => [ + 'url' => $sUrlWeb + ], + 'chrome_web_icon' => !empty($aMessage['icon']) ? $aMessage['icon'] : '', + ]; + + if ('on' == getParam('bx_nexus_option_push_notifications_count')) { + $iBadgeCount = $this->getNotificationsCount($iProfileId); + $aFields['ios_badgeType'] = 'SetTo'; + $aFields['ios_badgeCount'] = $iBadgeCount; + } + + $oChannel = curl_init(); + curl_setopt($oChannel, CURLOPT_URL, "https://onesignal.com/api/v1/notifications"); + curl_setopt($oChannel, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json; charset=utf-8', + 'Authorization: Basic ' . $this->_sRestApi + ]); + curl_setopt($oChannel, CURLOPT_RETURNTRANSFER, true); + curl_setopt($oChannel, CURLOPT_HEADER, false); + curl_setopt($oChannel, CURLOPT_POST, true); + curl_setopt($oChannel, CURLOPT_POSTFIELDS, json_encode($aFields)); + if (getParam('sys_curl_ssl_allow_untrusted') == 'on') + curl_setopt($oChannel, CURLOPT_SSL_VERIFYPEER, false); + + $sResult = curl_exec($oChannel); + curl_close($oChannel); + + $oResult = @json_decode($sResult, true); + if(isset($oResult['errors'])) + foreach($oResult['errors'] as $sError) { + bx_log('sys_push', $sError . " Message:" . json_encode($aMessage)); + } + + return $sResult; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScore.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScore.php new file mode 100644 index 0000000000..dc3b7dafba --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScore.php @@ -0,0 +1,594 @@ +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 BxDolScore extends BxDolObject +{ + protected static $_sCounterStyleSimple = 'simple'; // total counter only. + protected static $_sCounterStyleDivided = 'divided'; // counters [icon + counter] divided by action (up/down). + + protected $_aScore; + + protected $_aElementDefaults; + protected $_aElementDefaultsApi; + protected $_aElementParamsApi; //--- Params from DefaultsApi array to be passed to Api + + protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false) + { + parent::__construct($sSystem, $iId, $iInit, $oTemplate); + if(empty($this->_sSystem)) + return; + + $this->_oQuery = new BxDolScoreQuery($this); + } + + /** + * get votes object instanse + * @param $sSys vote object name + * @param $iId associated content id, where vote is available + * @param $iInit perform initialization + * @return null on error, or ready to use class instance + */ + public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false) + { + $sKey = 'BxDolScore!' . $sSys . $iId . ($oTemplate ? $oTemplate->getClassName() : ''); + if(isset($GLOBALS['bxDolClasses'][$sKey])) + return $GLOBALS['bxDolClasses'][$sKey]; + + $aSystems = self::getSystems(); + if(!isset($aSystems[$sSys])) + return null; + + $sClassName = 'BxTemplScore'; + if(!empty($aSystems[$sSys]['class_name'])) { + $sClassName = $aSystems[$sSys]['class_name']; + if(!empty($aSystems[$sSys]['class_file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']); + } + + $o = new $sClassName($sSys, $iId, $iInit, $oTemplate); + return ($GLOBALS['bxDolClasses'][$sKey] = $o); + } + + public static function &getSystems() + { + $sKey = 'bx_dol_cache_memory_score_systems'; + + if(!isset($GLOBALS[$sKey])) + $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_score', 'getAllWithKey', ' + SELECT + `id` as `id`, + `name` AS `name`, + `module` AS `module`, + `table_main` AS `table_main`, + `table_track` AS `table_track`, + `post_timeout` AS `post_timeout`, + `pruning` AS `pruning`, + `is_undo` AS `is_undo`, + `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_score` AS `trigger_field_score`, + `trigger_field_cup` AS `trigger_field_cup`, + `trigger_field_cdown` AS `trigger_field_cdown`, + `class_name` AS `class_name`, + `class_file` AS `class_file` + FROM `sys_objects_score`', 'name'); + + return $GLOBALS[$sKey]; + } + + public static function onAuthorDelete ($iAuthorId) + { + $aSystems = self::getSystems(); + foreach($aSystems as $sSystem => $aSystem) + self::getObjectInstance($sSystem, 0)->getQueryObject()->deleteAuthorEntries($iAuthorId); + + return true; + } + + public function isPerformed($iObjectId, $iAuthorId, $iAuthorIp = 0) + { + return parent::isPerformed($iObjectId, $iAuthorId) && !$this->_oQuery->isPostTimeoutEnded($iObjectId, $iAuthorId, $iAuthorIp); + } + + 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 isUndo() + { + return (int)$this->_aSystem['is_undo'] == 1; + } + + public function getStatCounterUp() + { + $aScore = $this->_getVote(); + return $aScore['count_up']; + } + + public function getStatCounterDown() + { + $aScore = $this->_getVote(); + return $aScore['count_down']; + } + + public function getStatScore() + { + $aScore = $this->_getVote(); + return $aScore['score']; + } + + public function getSocketName() + { + return $this->_sSystem . '_scores'; + } + + /** + * Actions functions + */ + public function actionVoteUp() + { + if(!$this->isEnabled()) + return echoJson(array('code' => 1, 'message' => _t('_sys_score_err_not_enabled'))); + + $aVoteData = ['type' => BX_DOL_SCORE_DO_UP]; + $aRequestParamsData = $this->_getRequestParamsData(); + + return echoJson($this->vote($aVoteData, $aRequestParamsData)); + } + + public function actionVoteDown() + { + if(!$this->isEnabled()) + return echoJson(array('code' => 1, 'message' => _t('_sys_score_err_not_enabled'))); + + $aVoteData = ['type' => BX_DOL_SCORE_DO_DOWN]; + $aRequestParamsData = $this->_getRequestParamsData(); + + return echoJson($this->vote($aVoteData, $aRequestParamsData)); + } + + public function actionGetVotedBy() + { + if (!$this->isEnabled()) + return ''; + + $aParams = $this->_getRequestParamsData(); + + if(($sType = bx_get('type')) !== false) { + $sType = bx_process_input($sType); + + if(in_array($sType, [BX_DOL_SCORE_DO_UP, BX_DOL_SCORE_DO_DOWN])) + $aParams['type'] = $sType; + } + + return $this->_getVotedBy($aParams); + } + + public function vote($aVoteData = [], $aRequestParamsData = []) + { + if(!$this->isAllowedVote(true)) + return ['code' => BX_DOL_OBJECT_ERR_ACCESS_DENIED, 'message' => $this->msgErrAllowedVote()]; + + $sType = $aVoteData['type']; + $iObjectId = $this->getId(); + $iObjectAuthorId = $this->getObjectAuthorId($iObjectId); + $iAuthorId = $this->_getAuthorId(); + $iAuthorIp = $this->_getAuthorIp(); + + $bUndo = $this->isUndo(); + $bVoted = $this->isPerformed($iObjectId, $iAuthorId, $iAuthorIp); + $bPerformUndo = $bVoted && $bUndo; + + if(!$bPerformUndo && !$this->isAllowedVote()) + return ['code' => BX_DOL_OBJECT_ERR_ACCESS_DENIED, 'message' => $this->msgErrAllowedVote()]; + + if($bVoted && !$bUndo) + return ['code' => BX_DOL_OBJECT_ERR_DUPLICATE, 'message' => _t('_sys_score_err_duplicate_vote')]; + + if($bPerformUndo) { + $aTrack = $this->_getTrack($iObjectId, $iAuthorId); + if(!empty($aTrack) && is_array($aTrack)) + $aVoteData = array_intersect_key($aTrack, $aVoteData); + } + + $iId = $this->_putVoteData($iObjectId, $iAuthorId, $iAuthorIp, $aVoteData, $bPerformUndo); + if($iId === false) + return ['code' => BX_DOL_OBJECT_ERR_CANNOT_PERFORM]; + + if(!$bPerformUndo) + $this->isAllowedVote(true); + + $this->_trigger(); + + $sTypeUc = ucfirst($sType); + /** + * @hooks + * @hookdef hook-bx_dol_score-doVoteUp '{object_name}', 'doVoteUp' - hook after score vote + * - $unit_name - score object name + * - $action - equals `doVoteUp` + * - $object_id - object id which got a vote + * - $sender_id - profile id who voted + * - $extra_params - array of additional params with the following array keys: + * - `score_id` - [int] vote id + * - `score_author_id` - [int] profile id who voted + * - `object_author_id` - [int] author id of the object which got a vote + * @hook @ref hook-bx_dol_score-doVoteUp + */ + /** + * @hooks + * @hookdef hook-bx_dol_score-doVoteDown '{object_name}', 'doVoteDown' - hook after score vote + * It's equivalent to @ref hook-bx_dol_score-doVoteUp + * @hook @ref hook-bx_dol_score-doVoteDown + */ + /** + * @hooks + * @hookdef hook-bx_dol_score-undoVoteUp '{object_name}', 'undoVoteUp' - hook after undo score vote + * It's equivalent to @ref hook-bx_dol_score-doVoteUp + * @hook @ref hook-bx_dol_score-undoVoteUp + */ + /** + * @hooks + * @hookdef hook-bx_dol_score-undoVoteDown '{object_name}', 'undoVoteDown' - hook after undo score vote + * It's equivalent to @ref hook-bx_dol_score-doVoteUp + * @hook @ref hook-bx_dol_score-undoVoteDown + */ + bx_alert($this->_sSystem, ($bPerformUndo ? 'un' : '') . 'doVote' . $sTypeUc, $iObjectId, $iAuthorId, [ + 'score_id' => $iId, + 'score_author_id' => $iAuthorId, + 'object_author_id' => $iObjectAuthorId + ]); + + /** + * @hooks + * @hookdef hook-score-doUp 'score', 'doUp' - hook after score vote + * - $unit_name - equals `score` + * - $action - equals `doUp` + * - $object_id - score vote id + * - $sender_id - profile id who voted + * - $extra_params - array of additional params with the following array keys: + * - `object_system` - [string] vote object name + * - `object_id` - [int] object id which got a vote + * - `object_author_id` - [int] author id of the object which got a vote + * @hook @ref hook-score-doUp + */ + /** + * @hooks + * @hookdef hook-score-doDown 'score', 'doDown' - hook after score vote + * It's equivalent to @ref hook-score-doUp + * @hook @ref hook-score-doDown + */ + /** + * @hooks + * @hookdef hook-score-undoUp 'score', 'undoUp' - hook after undo score vote + * It's equivalent to @ref hook-score-doUp + * @hook @ref hook-score-undoUp + */ + /** + * @hooks + * @hookdef hook-score-undoDown 'score', 'undoDown' - hook after undo score vote + * It's equivalent to @ref hook-score-doUp + * @hook @ref hook-score-undoDown + */ + bx_alert('score', ($bPerformUndo ? 'un' : '') . 'do' . $sTypeUc, $iId, $iAuthorId, [ + 'object_system' => $this->_sSystem, + 'object_id' => $iObjectId, + 'object_author_id' => $iObjectAuthorId + ]); + + $aRequestParamsData['show_script'] = false; + + $bVoted = !$bVoted; + $aScore = $this->_getVote($iObjectId, true); + $iCup = (int)$aScore['count_up']; + $iCdown = (int)$aScore['count_down']; + + $aResult = [ + 'code' => 0, + 'type' => $sType, + 'score' => $aScore['score'], + 'scoref' => $iCup > 0 || $iCdown > 0 ? $this->_getCounterLabel($aScore['score'], $aRequestParamsData) : '', + 'cup' => $iCup, + 'cdown' => $iCdown, + 'counter' => $this->getCounter($aRequestParamsData), + 'label_icon' => $this->_getIconDo($sType), + 'label_title' => _t($this->_getTitleDo($sType)), + 'voted' => $bVoted, + 'disabled' => $bVoted && !$bUndo, + ]; + + $aResult['api'] = [ + 'performer_id' => $iAuthorId, + 'is_voted' => $aResult['voted'], + 'is_disabled' => $aResult['disabled'], + $sType => [ + 'icon' => $aResult['label_icon'], + 'title' => $aResult['label_title'], + ], + 'counter' => $this->getVote() + ]; + + if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()) + $oSockets->sendEvent($this->getSocketName(), $iObjectId, 'voted', json_encode($this->_returnVoteDataForSocket($aResult))); + + return $aResult; + } + + + /** + * Permissions functions + */ + public function isAllowedVote($isPerformAction = false) + { + if(isAdmin()) + return true; + + if(!$this->checkAction('vote', $isPerformAction)) + return false; + + $aObject = $this->_oQuery->getObjectInfo($this->_iId); + if(empty($aObject) || !is_array($aObject)) + return false; + + return $this->_isAllowedVoteByObject($aObject); + } + + public function msgErrAllowedVote() + { + $sMsg = $this->checkActionErrorMsg('vote'); + if(empty($sMsg)) + $sMsg = _t('_sys_txt_access_denied'); + + return $sMsg; + } + + public function isAllowedVoteView($isPerformAction = false) + { + if(isAdmin()) + return true; + + return $this->checkAction('vote_view', $isPerformAction); + } + + public function msgErrAllowedVoteView() + { + return $this->checkActionErrorMsg('vote_view'); + } + + public function isAllowedVoteViewVoters($isPerformAction = false) + { + if(isAdmin()) + return true; + + return $this->checkAction('vote_view_voters', $isPerformAction); + } + + public function msgErrAllowedVoteViewVoters() + { + return $this->checkActionErrorMsg('vote_view_voters'); + } + + + /** + * Internal functions + */ + protected function _isAllowedVoteByObject($aObject) + { + return bx_srv($this->_aSystem['module'], 'check_allowed_view_for_profile', [$aObject]) === CHECK_ACTION_RESULT_ALLOWED; + } + + protected function _putVoteData($iObjectId, $iAuthorId, $iAuthorIp, $aData, $bPerformUndo) + { + return $this->_oQuery->putVote($iObjectId, $iAuthorId, $iAuthorIp, $aData, $bPerformUndo); + } + + protected function _returnVoteDataForSocket($aData, $aMask = []) + { + if(empty($aMask) || !is_array($aMask)) + $aMask = ['code', 'type', 'score', 'scoref', 'cup', 'cdown', 'counter', 'api']; + + return array_intersect_key($aData, array_flip($aMask)); + } + + protected function _getVote($iObjectId = 0, $bForceGet = false) + { + if(!empty($this->_aScore) && !$bForceGet) + return $this->_aScore; + + if(empty($iObjectId)) + $iObjectId = $this->getId(); + + $this->_aScore = $this->_oQuery->getScore($iObjectId); + return $this->_aScore; + } + + protected function _isVote($iObjectId = 0, $bForceGet = false) + { + $aScore = $this->_getVote($iObjectId, $bForceGet); + + return $this->_isCount($aScore); + } + + protected function _isCount($aScore = []) + { + if(empty($aScore)) + $aScore = $this->_getVote(); + + return (isset($aScore['count_up']) && (int)$aScore['count_up'] != 0) || (isset($aScore['count_down']) && (int)$aScore['count_down'] != 0); + } + + protected function _getTrack($iObjectId, $iAuthorId) + { + return $this->_oQuery->getTrack($iObjectId, $iAuthorId); + } + + /** + * Note. By default image based controls aren't used. + * Therefore it can be overwritten in custom template. + */ + protected function _getImageDo($sType) + { + $sResult = ''; + + switch($sType) { + case BX_DOL_SCORE_DO_UP: + $sResult = ''; + break; + case BX_DOL_SCORE_DO_DOWN: + $sResult = ''; + break; + } + + return $sResult; + } + + protected function _getIconDo($sType = '') + { + $sResult = ''; + + switch($sType) { + case BX_DOL_SCORE_DO_UP: + $sResult = 'arrow-up'; + break; + + case BX_DOL_SCORE_DO_DOWN: + $sResult = 'arrow-down'; + break; + + default: + $sResult = 'arrows-alt-v'; + } + + return $sResult; + } + + protected function _getTitleDo($sType) + { + $sResult = ''; + + switch($sType) { + case BX_DOL_SCORE_DO_UP: + $sResult = '_sys_score_do_up'; + break; + case BX_DOL_SCORE_DO_DOWN: + $sResult = '_sys_score_do_down'; + break; + } + + return $sResult; + } + + protected function _getTitleDoBy() + { + return '_sys_score_do_by'; + } + + protected function _encodeElementParams($aParams) + { + if(empty($aParams) || !is_array($aParams)) + return ''; + + return urlencode(base64_encode(serialize($aParams))); + } + + protected function _decodeElementParams($sParams, $bMergeWithDefaults = true) + { + $aParams = array(); + if(!empty($sParams)) + $aParams = unserialize(base64_decode(urldecode($sParams))); + + if(empty($aParams) || !is_array($aParams)) + $aParams = array(); + + if($bMergeWithDefaults) + $aParams = array_merge($this->_aElementDefaults, $aParams); + + return $aParams; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScoreQuery.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScoreQuery.php new file mode 100644 index 0000000000..efe85f912d --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolScoreQuery.php @@ -0,0 +1,187 @@ +_oModule->getSystemInfo(); + $this->_sTriggerFieldScore = $aSystem['trigger_field_score']; + $this->_sTriggerFieldCup = $aSystem['trigger_field_cup']; + $this->_sTriggerFieldCdown = $aSystem['trigger_field_cdown']; + + $this->_iPostTimeout = (int)$aSystem['post_timeout']; + + $this->_sMethodGetEntry = 'getScore'; + } + + public function getPerformedBy($iObjectId, $aParams = [], $iStart = 0, $iPerPage = 0) + { + $aBindings = [ + 'object_id' => $iObjectId + ]; + $sWhereClause = " AND `object_id`=:object_id"; + + if(!empty($aParams['type'])) { + $sWhereClause .= " AND `type`=:type"; + $aBindings['type'] = $aParams['type']; + } + + $sLimitClause = ""; + if(!empty($aParams['per_page'])) + $sLimitClause = $this->prepareAsString(" LIMIT ?, ?", $aParams['start'], $aParams['per_page']); + + $sQuery = "SELECT + `author_id` AS `id`, + `type` AS `vote_type`, + `date` AS `vote_date` + FROM `{$this->_sTableTrack}` + WHERE 1" . $sWhereClause . $sLimitClause; + + return $this->getAll($sQuery, $aBindings); + } + + public function isPostTimeoutEnded($iObjectId, $iAuthorId, $sAuthorIp) + { + if($this->_iPostTimeout == 0) + return true; + + $aBindings = array( + 'object_id' => $iObjectId, + 'date' => time() - $this->_iPostTimeout + ); + $sWhereClause = " AND `object_id` = :object_id AND `date` > :date"; + + if(!empty($iAuthorId)) { + $aBindings['author_id'] = $iAuthorId; + + $sWhereClause .= " AND `author_id` = :author_id"; + } + else { + $aBindings['author_nip'] = bx_get_ip_hash($sAuthorIp); + + $sWhereClause .= " AND `author_nip` = :author_nip"; + } + + return (int)$this->getOne("SELECT `object_id` FROM `" . $this->_sTableTrack . "` WHERE 1" . $sWhereClause, $aBindings) == 0; + } + + public function getScore($iObjectId) + { + $aResult = $this->getRow("SELECT `count_up`, `count_down`, `count_up` - `count_down` AS `score` FROM {$this->_sTable} WHERE `object_id` = :object_id LIMIT 1", array( + 'object_id' => $iObjectId + )); + + if(empty($aResult) || !is_array($aResult)) + $aResult = array('count_up' => 0, 'count_down' => 0, 'score' => 0); + + return $aResult; + } + + public function putVote($iObjectId, $iAuthorId, $sAuthorIp, $aData, $bUndo = false) + { + $bExists = (int)$this->getOne("SELECT `object_id` FROM `{$this->_sTable}` WHERE `object_id` = :object_id LIMIT 1", ['object_id' => $iObjectId]) != 0; + if(!$bExists && $bUndo) + return false; + + $sType = $aData['type']; + + if(!$bExists) + $sQuery = $this->prepare("INSERT INTO {$this->_sTable} SET `object_id` = ?, `count_" . $sType . "` = '1'", $iObjectId); + else + $sQuery = $this->prepare("UPDATE `{$this->_sTable}` SET `count_" . $sType . "` = `count_" . $sType . "` " . ($bUndo ? "-" : "+") . " 1 WHERE `object_id` = ?", $iObjectId); + + if((int)$this->query($sQuery) == 0) + return false; + + if($bUndo) + return $this->_deleteTrack($iObjectId, $iAuthorId); + + if((int)$this->query("INSERT INTO `{$this->_sTableTrack}` SET " . $this->arrayToSQL([ + 'object_id' => $iObjectId, + 'author_id' => $iAuthorId, + 'author_nip' => bx_get_ip_hash($sAuthorIp), + 'type' => $sType, + 'date' => time() + ])) > 0) + return $this->lastId(); + + return false; + } + + public function getLegend($iObjectId) + { + $sQuery = $this->prepare("SELECT `type` AS `type`, COUNT(`type`) AS `count` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? GROUP BY `type`", $iObjectId); + + return $this->getAllWithKey($sQuery, 'type'); + } + + public function getSqlParts($sMainTable, $sMainField) + { + $aResult = parent::getSqlParts($sMainTable, $sMainField); + if(empty($aResult)) + return $aResult; + + $aResult['fields'] = ", `{$this->_sTable}`.`count_up` AS `score_cup`, `{$this->_sTable}`.`count_down` AS `score_cdown`, (`{$this->_sTable}`.`count_up` - `{$this->_sTable}`.`count_down`) AS `score` "; + return $aResult; + } + + protected function _deleteTrack($iObjectId, $iAuthorId) + { + $iId = (int)$this->getOne("SELECT `id` FROM `{$this->_sTableTrack}` WHERE `object_id`=:object_id AND `author_id`=:author_id LIMIT 1", [ + 'object_id' => $iObjectId, + 'author_id' => $iAuthorId + ]); + + if((int)$this->query("DELETE FROM `{$this->_sTableTrack}` WHERE `id`=:id LIMIT 1", ['id' => $iId]) > 0) + return $iId; + + return false; + } + + protected function _updateTriggerTable($iObjectId, $aEntry) + { + $aSet = array( + $this->_sTriggerFieldScore => $aEntry['score'], + $this->_sTriggerFieldCup => $aEntry['count_up'], + $this->_sTriggerFieldCdown => $aEntry['count_down'] + ); + + return (int)$this->query("UPDATE `{$this->_sTriggerTable}` SET " . $this->arrayToSQL($aSet) . " WHERE `{$this->_sTriggerFieldId}` = :object_id", array( + 'object_id' => $iObjectId + )) > 0; + } + + protected function _deleteAuthorEntriesTableMain($aTrack) + { + return $this->query("UPDATE `{$this->_sTable}` SET `count_" . $aTrack['type'] . "`=`count_" . $aTrack['type'] . "`-1 WHERE `object_id`=:object_id", array( + 'object_id' => $aTrack['object_id'] + )); + } + + protected function _deleteAuthorEntriesTableTrigger($aTrack) + { + $aScore = $this->getScore($aTrack['object_id']); + + return $this->_updateTriggerTable($aTrack['object_id'], $aScore); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSearch.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSearch.php new file mode 100644 index 0000000000..032b083443 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSearch.php @@ -0,0 +1,1426 @@ +_bIsApi = bx_is_api(); + + $this->aClasses = BxDolDb::getInstance()->fromCache('sys_objects_search', 'getAllWithKey', + 'SELECT `ID` as `id`, + `Title` as `title`, + `ClassName` as `class`, + `ClassPath` as `file`, + `ObjectName`, + `GlobalSearch` + FROM `sys_objects_search` + ORDER BY `Order` ASC', 'ObjectName' + ); + + if (is_array($aChoice) && !empty($aChoice)) { + foreach ($aChoice as $sValue) { + if (isset($this->aClasses[$sValue])) + $this->aChoice[$sValue] = $this->aClasses[$sValue]; + } + } else { + $this->aChoice = $this->aClasses; + } + } + + /** + * create units for all classes and calling their processing methods + */ + public function response () + { + $sCode = $this->_bDataProcessing ? [] : ''; + + if($this->_bLiveSearch && ($iAssistant = BxDolAI::getAssistantForLiveSearch()) != 0) { + $sKeyword = ''; + if(($sKeyword = bx_get('keyword')) !== false) + $sKeyword = bx_process_input($sKeyword); + + $sCode .= BxDolAIAssistant::getObjectInstance($iAssistant)->getAskButton($sKeyword); + } + + $bSingle = count($this->aChoice) == 1; + foreach($this->aChoice as $sKey => $aValue) { + if(!$this->_sMetaType && !$aValue['GlobalSearch']) + continue; + + $sClassName = 'BxTemplSearchResult'; + if(!empty($aValue['class'])) { + $sClassName = $aValue['class']; + if(!empty($aValue['file'])) + require_once(BX_DIRECTORY_PATH_ROOT . $aValue['file']); + } + + $oEx = new $sClassName(); + if($this->_sMetaType && !$oEx->isMetaEnabled($this->_sMetaType)) + continue; + + $oEx->setId($aValue['id']); + $oEx->setLiveSearch($this->_bLiveSearch); + $oEx->setMetaType($this->_sMetaType); + $oEx->setCategoryObject($this->_sCategoryObject); + $oEx->setCenterContentUnitSelector(false); + $oEx->setSingleSearch($bSingle); + $oEx->setCustomSearchCondition($this->_aCustomSearchCondition); + + $oEx->aCurrent = array_merge_recursive($oEx->aCurrent, $this->_aCustomCurrentCondition); + if($this->_sUnitTemplate) + $oEx->setUnitTemplate($this->_sUnitTemplate); + + if($this->_bDataProcessing) { + if($this->_bIsApi) { + if($bSingle) + $sCode = $oEx->decodeDataAPI($oEx->getSearchData()); + else + $sCode[$sKey] = $oEx->getSearchQuery($sKey); + } + else + $sCode[$sKey] = $oEx->getSearchData(); + } + else + $sCode .= $this->_bRawProcessing ? $oEx->processingRaw() : $oEx->processing(); + } + + if($this->_bIsApi && !$bSingle && is_array($sCode)) { + $bExtendedUnits = getParam('sys_api_extended_units') == 'on'; + + $aQueries = []; + foreach($sCode as $aQuery) + if(!empty($aQuery['query'])) + $aQueries[] = $aQuery['query']; + + $aItems = BxDolDb::getInstance()->getAll('(' . implode(') UNION (', $aQueries) . ') ORDER BY `added` DESC ' . current($sCode)['limit']); + + $sCode = []; + foreach($aItems as $aItem) + if(($oContentInfo = BxDolContentInfo::getObjectInstance($aItem['content_info'])) !== false) + $sCode[] = $oContentInfo->getContentInfoAPI($aItem['id'], $bExtendedUnits); + } + + return $sCode; + } + + public function getEmptyResult () + { + $sKey = _t('_Empty'); + return DesignBoxContent($sKey, MsgBox($sKey), BX_DB_PADDING_DEF); + } + + protected function getKeyTitlesPairs () + { + $a = array(); + foreach ($this->aChoice as $sKey => $r) + if ($this->_sMetaType || $r['GlobalSearch']) + $a[$sKey] = _t($r['title']); + return $a; + } + + public function setLiveSearch($bLiveSearch) + { + $this->_bLiveSearch = $bLiveSearch; + } + + public function setMetaType($s) + { + $aMetaTypes = array('location_country', 'location_country_city', 'location_country_state', 'mention', 'keyword'); + if (in_array($s, $aMetaTypes)) + $this->_sMetaType = $s; + } + + public function setCategoryObject($s) + { + $this->_sCategoryObject = $s; + } + + /** + * Set custom search condition to use istead of GET/POST variables + * @param $a array of params, such as 'keyword', 'state', 'city' + * @return nothing + */ + public function setCustomSearchCondition($a) + { + $this->_aCustomSearchCondition = $a; + } + + /** + * Set custom data for aCurrent array + * @param $a array of params + * @return nothing + */ + public function setCustomCurrentCondition($a) + { + $this->_aCustomCurrentCondition = $a; + } + + /** + * Set custom unit template + * @param $s template name + * @return nothing + */ + public function setUnitTemplate($s) + { + $this->_sUnitTemplate = $s; + } + + /** + * Display search results without design box and paginate + */ + public function setRawProcessing($b) + { + $this->_bRawProcessing = $b; + } + + /** + * Return search result as array of data + */ + public function setDataProcessing($b) + { + $this->_bDataProcessing = $b; + } +} + +/* + * Search class for processing search requests and displaying search results. + * + * Allows present content from modules on search params or internal (via fields of class) conditions. + * + * Example of usage (you can see example in any of default Dolhpin's modules): + * + * 1. Extends your own search class from this one (or from BxBaseSearchResult or BxBaseSearchResultSharedMedia classes) + * 2. Set necessary fields of class (using as example BxFilesSearch): + * + * @code + * + * // main field of search class + * $this->aCurrent = array( + * + * // name of module + * 'name' => 'bx_files', + * + * // language key with name of module + * 'title' => '_bx_files', + * + * // main content table + * 'table' => 'bx_files_main', + * + * // array of all fields which can be choosen for select in result query + * 'ownFields' => array('ID', 'Title', 'Uri', 'Desc', 'Date', 'Size', 'Ext', 'Views', 'Rate', 'RateCount', 'Type'), + * + * // array of fields which take a part in search by keyword (global search and related words only - in other cases leave it blank) + * 'searchFields' => array('Title', 'Tags', 'Desc', 'Categories'), + * + * // array of join tables + * // 'type' - type of join + * // 'table' - join table + * // 'mainField' - field from main table for 'on' condition + * // 'onField' - field from joining table for 'on' condition + * // 'joinFields' - array of fields from joining table + * + * 'join' => array( + * 'profile' => array( + * 'type' => 'left', + * 'table' => 'Profiles', + * 'mainField' => 'Owner', + * 'onField' => 'ID', + * 'joinFields' => array('NickName'), + * ), + * // ... + * ), + * + * // array of search parameters + * // 'value' - value of search parameter (can be number, string or array) + * // 'field' - field which will take value of search, may be lefy as blank then in search query will be pasted to WHERE conditon via operator AGAINST + * // 'operator' - operator between field and value (can be 'in', 'not in', '>', '<', 'against', 'like' and '=' by default), + * // 'paramName' - GET param which will keep 'value' for pagianation + * + * 'restriction' => array( + * 'activeStatus' => array('value'=>'approved', 'field'=>'Status', 'operator'=>'=', 'paramName' => 'status'), + * 'albumType' => array('value'=>'', 'field'=>'Type', 'operator'=>'=', 'paramName'=>'albumType', 'table'=>'sys_albums'), + * ), + * + * // array of pagination + * // 'perPage' - units per page + * // 'start' - show search results starting from this number + * // 'num' - number of element to show on current page + 1 + * + * 'paginate' => array('perPage' => 10, 'start' => 0, 'num' => 11), + * + * // sort mode - by default it was last (DESC by date) + * 'sorting' => 'last', + * + * // mode of units presentation in search result, can be 'short' or 'full' view + * 'view' => 'full', + * + * // field of id + * 'ident' => 'ID', + * + * // rss feed array + * 'rss' => array( + * 'title' => '', + * 'link' => '', + * 'image' => '', + * 'profile' => 0, + * 'fields' => array ( + * 'Link' => '', + * 'Title' => 'Title', + * 'DateTimeUTS' => 'Date', + * 'Desc' => 'Desc', + * 'Photo' => '', + * ), + * ), + * ); + * + * // array of fields renamings + * $aPseud - filling in costructor by function _getPseud + * + * // unique identificator from `sys_objects_search` table + * $id; + * + * @endcode + * + * Memberships/ACL: + * Doesn't depend on user's membership. + * + * Alerts: + * no alerts available + * + */ + +class BxDolSearchResult implements iBxDolReplaceable +{ + public $aCurrent; ///< search results configuration + + protected $aPseud; ///< array of fields renamings + protected $id; ///< unique identificator of block on mixed search results page + protected $bDisplayEmptyMsg = false; ///< display empty message instead of nothing, when no results + protected $sDisplayEmptyMsgKey = ''; ///< custom empty message language key, instead of default "empty" message + protected $bProcessPrivateContent = true; ///< check each item for privacy, if view isn't allowed then display private view instead + protected $aPrivateConditionsIndexes = array('restriction' => array(), 'join' => array()); ///< conditions indexes for bProcessPrivateContent + protected $bForceAjaxPaginate = false; ///< force ajax paginate + protected $iPaginatePerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT; ///< default 'per page' value for paginate. + + protected $_bIsApi; + protected $_bSingleSearch = true; + protected $_bLiveSearch = false; + protected $_sMetaType = ''; + protected $_sMode = ''; + protected $_aParams = []; + protected $_sCategoryObject = ''; + protected $_aCustomSearchCondition = array(); + protected $_bValidate = false; + + protected $_aMarkers = array (); ///< markers to replace somewhere, usually title and browse url (defined in custom class) + + /** + * constructor + * filling identificator field + */ + function __construct () + { + $this->_bIsApi = bx_is_api(); + + if (isset($this->aPseud['id'])) + $this->aCurrent['ident'] = $this->aPseud['id']; + } + + public function getId() + { + return $this->id; + } + + public function setId($sId) + { + $this->id = $sId; + } + + public function getModuleName() + { + return isset($this->aCurrent['module_name']) ? $this->aCurrent['module_name'] : ''; + } + + public function getContentInfoName() + { + return isset($this->aCurrent['name']) ? $this->aCurrent['name'] : ''; + } + + public function setAjaxPaginate($b = true) + { + $this->bForceAjaxPaginate = $b; + } + + public function setPaginatePerPage($iPerPage) + { + $this->iPaginatePerPage = $iPerPage; + } + + public function setSingleSearch($bSingleSearch) + { + $this->_bSingleSearch = $bSingleSearch; + } + + public function setLiveSearch($bLiveSearch) + { + $this->_bLiveSearch = $bLiveSearch; + } + + public function setMetaType($s) + { + $this->_sMetaType = $s; + } + + public function isMetaEnabled($s) + { + if (empty($this->aCurrent['object_metatags'])) + return false; + + if (!($o = BxDolMetatags::getObjectInstance($this->aCurrent['object_metatags']))) + return false; + + switch ($s) { + case 'location_country_city': + case 'location_country_state': + case 'location_country': + return $o->locationsIsEnabled(); + case 'mention': + return $o->mentionsIsEnabled(); + case 'keyword': + return $o->keywordsIsEnabled(); + } + + return false; + } + + public function setCategoryObject($s) + { + $this->_sCategoryObject = $s; + } + + public function setCustomSearchCondition($a) + { + $this->_aCustomSearchCondition = $a; + } + + public function setCustomCurrentCondition($a) + { + $this->aCurrent = array_merge_recursive($this->aCurrent, $a); + } + + public function setCategoriesCondition($sKeyword) + { + $this->aCurrent['join']['multicat'] = array( + 'type' => 'INNER', + 'table' => 'sys_categories2objects', + 'mainField' => 'id', + 'onField' => 'object_id', + 'joinFields' => array(), + ); + + if (isset($this->aCurrent['tableSearch'])){ + $this->aCurrent['join']['multicat']['mainTable'] = $this->aCurrent['tableSearch']; + } + + $this->aCurrent['join']['multicat2'] = array( + 'type' => 'INNER', + 'table' => 'sys_categories', + 'mainField' => 'category_id', + 'mainTable' => 'sys_categories2objects', + 'onField' => 'id', + 'joinFields' => array(), + ); + $this->aCurrent['restriction']['multicat'] = array('value' => $sKeyword, 'field' => 'value', 'operator' => '=', 'table' => 'sys_categories'); + } + + /** + * Display empty message if there is no content, custom empty message can be used. + * @param $b - boolan value to enable or disable 'empty' message + * @param $sLangKey [optional] - custom 'empty' message + */ + public function setDisplayEmptyMsg($b, $sLangKey = '') + { + $this->bDisplayEmptyMsg = $b; + if ($sLangKey) + $this->sDisplayEmptyMsgKey = $sLangKey; + } + + /** + * Perform privacy checking for every unit + * @param $b - boolan value to enable or disable privacy checking + */ + public function setProcessPrivateContent ($b) + { + $this->bProcessPrivateContent = $b; + if ($b) { + // unset condition which was set when 'bProcessPrivateContent' was set to 'false' + foreach ($this->aPrivateConditionsIndexes['restriction'] as $sKey) + unset($this->aCurrent['restriction'][$sKey]); + foreach ($this->aPrivateConditionsIndexes['join'] as $sKey) + unset($this->aCurrent['join'][$sKey]); + } + } + + /** + * Get search results without design box and paginate + * @return html code + */ + function processingRaw () + { + $sCode = $this->displayResultBlock(); + return $this->aCurrent['paginate']['num'] > 0 ? $sCode : ''; + } + + /** + * Get html box of search results (usually used in grlobal search) + * @return html code + */ + function processing () + { + if($this->_bIsApi) + return $this->processingAPI(); + + if($this->_bValidate) + return $this->getSearchData(); + + $sCode = $this->displayResultBlock(); + if ($this->aCurrent['paginate']['num'] > 0) { + $sPaginate = $this->showPagination(); + $sCode = $this->displaySearchBox($sCode, $sPaginate); + } + else { + $sCode = $this->bDisplayEmptyMsg ? $this->displaySearchBox($this->displayResultBlockEmpty()) : ''; + } + + return $sCode; + } + + function processingAPI () + { + $sModule = 'system'; + $sUnitType = 'content'; + if(!empty($this->oModule)) { + $sModule = $this->oModule->getName(); + + if(method_exists($this->oModule, 'serviceActAsProfile') && $this->oModule->serviceActAsProfile()) + $sUnitType = 'profile'; + if(method_exists($this->oModule, 'serviceIsGroupProfile') && $this->oModule->serviceIsGroupProfile()) + $sUnitType = 'context'; + } + + $sUnit = 'list'; + if(in_array($this->sUnitViewDefault, ['showcase', 'gallery'])) + $sUnit = 'card'; + + $aData = defined('BX_API_PAGE') ? [] : $this->decodeDataAPI($this->getSearchData()); + + $aParams = [ + 'per_page' => $this->aCurrent['paginate']['perPage'], + 'start' => $this->aCurrent['paginate']['start'], + 'type' => $this->_sMode, + ]; + + if (isset($this->_aParams['author'])){ + $aParams['author'] = $this->_aParams['author']; + } + + if (isset($this->_aParams['category'])){ + $aParams['category'] = $this->_aParams['category']; + } + + return [ + 'module' => $sModule, + 'unit' => 'general-' . $sUnitType . '-' . $sUnit, + 'request_url' => !empty($this->aCurrent['api_request_url']) ? $this->aCurrent['api_request_url'] : '/api.php?r=' . $sModule . '/browse/¶ms[]=', + 'data' => $aData, + 'params' => $aParams + ]; + } + + function decodeDataAPI ($a) + { + return $a; + } + + /** + * Get html output of search result + * @return html code + */ + function displayResultBlock () + { + $aData = $this->getSearchData(); + + $sCode = ''; + if (count($aData) > 0) { + $sCode = $this->addCustomParts(); + foreach ($aData as $iKey => $aValue) { + $sCode .= $this->displaySearchUnit($aValue); + } + } + return $sCode; + } + + /** + * Get html output of empty search result + * @return html code + */ + function displayResultBlockEmpty () + { + return MsgBox(_t($this->sDisplayEmptyMsgKey ? $this->sDisplayEmptyMsgKey : '_Empty')); + } + + /** + * Add different code before html output (usually redeclared) + * no return result + */ + function addCustomParts () + { + + } + + /** + * Get XML string for rss output + */ + function rss () + { + if (!isset($this->aCurrent['rss']['fields']) || !isset($this->aCurrent['rss']['link'])) + return ''; + + $aData = $this->getSearchData(); + $f = &$this->aCurrent['rss']['fields']; + if ($aData) { + foreach ($aData as $k => $a) { + $aData[$k][$f['Link']] = $this->getRssUnitLink ($a); + + if(isset($f['Image'])) + $aData[$k][$f['Image']] = $this->getRssUnitImage ($a, $f['Image']); + } + } + + $oRss = new BxDolRssFactory (); + + return $oRss->GenRssByCustomData( + $aData, + isset($this->aCurrent['rss']['title']) && $this->aCurrent['rss']['title'] ? $this->aCurrent['rss']['title'] : $this->aCurrent['title'], + $this->aCurrent['rss']['link'], + $this->aCurrent['rss']['fields'], + isset($this->aCurrent['rss']['image']) ? $this->aCurrent['rss']['image'] : '', + isset($this->aCurrent['rss']['profile']) ? $this->aCurrent['rss']['profile'] : 0 + ); + } + + /** + * Output RSS XML with XML header + */ + function outputRSS () + { + header('Content-Type: text/xml; charset=UTF-8'); + echo $this->rss(); + } + + /** + * Return rss unit link (redeclared) + */ + function getRssUnitLink (&$a) + { + // override this functions to return permalink to rss unit + } + + /** + * Return rss unit image (redeclared) + */ + function getRssUnitImage (&$a, $sField) + { + // override this functions to return image for rss unit + } + + /** + * Naming fields in query's body + * @param string $sFieldName name of field + * @param string $sTableName name of field's table + * $param string $sOperator of field's calculation (like MAX) + * @param boolean $bRenameMode indicator for renaming and unsetting fields from field of class $this->aPseud + * return $sqlUnit sql code and unsetting elements from aPseud field + */ + function setFieldUnit ($sFieldName, $sTableName, $sOperator = '', $bRenameMode = true) + { + $bOperator = !empty($sOperator); + + $sqlUnit = "`$sTableName`.`$sFieldName`"; + if($bOperator) + $sqlUnit = $sOperator . '(' . $sqlUnit . ')'; + + if(!empty($this->aPseud) && $bRenameMode !== false && ($sKey = array_search($sFieldName . ($bOperator ? '_' . strtolower($sOperator) : ''), $this->aPseud)) !== false) { + $sqlUnit .= " as `$sKey`"; + unset($this->aPseud[$sKey]); + } + + return $sqlUnit . ', '; + } + + /** + * Get html code of of every search unit + * @param array $aData array of every search unit + * return html code + */ + function displaySearchUnit ($aData) + { + + } + + /** + * Get html code of search box with search results + * @param string $sCode html code of search results + * $param $sPaginate html code of paginate + * return html code + */ + function displaySearchBox ($sCode, $sPaginate = '') + { + + } + + /** + * Get html code of pagination + */ + function showPagination ($bAdmin = false, $bChangePage = true, $bPageReload = true) + { + + } + + /** + * Get array of data with search results + * return array with data + */ + function getSearchData () + { + /** + * @hooks + * @hookdef hook-simple_search-before_get_data 'simple_search', 'before_get_data' - hook on before after get data + * - $unit_name - equals `simple_search` + * - $action - equals `before_get_data` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `object` - [array] by ref, array of parameters, can be overridden in hook processing + * - `mode` - [string] search mode + * @hook @ref hook-simple_search-before_get_data + */ + bx_alert('simple_search', 'before_get_data', 0, false, [ + 'object' => &$this->aCurrent, + 'mode' => $this->_sMode + ]); + + if(($this->aPseud = $this->_getPseudFromParam()) === false) + $this->aPseud = $this->_getPseud(); + + $this->setConditionParams(); + $aData = $this->aCurrent['paginate']['num'] > 0 ? $this->getSearchDataByParams() : []; + + if($this->_bValidate) { + $aIds = array_map(function($aItem) { + return $aItem['id']; + }, $aData); + + sort($aIds); + sort($this->_aParams['validate']); + $aData = $aIds == $this->_aParams['validate'] ? 'valid' : 'invalid'; + } + + /** + * @hooks + * @hookdef hook-simple_search-get_data 'simple_search', 'get_data' - hook on after get data + * - $unit_name - equals `simple_search` + * - $action - equals `get_data` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `object` - [array] by ref, array of parameters, can be overridden in hook processing + * - `mode` - [string] search mode + * - `search_object` - [string] search object name + * @hook @ref hook-simple_search-get_data + */ + bx_alert('simple_search', 'get_data', 0, false, [ + 'object' => &$this->aCurrent, + 'mode' => $this->_sMode, + 'search_results' => &$aData + ]); + + return $aData; + } + + /** + * Get query [query, limit] for search results. Is used for combined search from different sections. + * @param type $aParams array with params + * @return type array with query and limit + */ + function getSearchQuery($sObject, $aParams = []) + { + if(!is_array($aParams)) + $aParams = []; + $aParams = array_merge($aParams, ['for_union' => true]); + + $this->aPseud = [ + 'id' => !empty($this->aCurrent['ident']) ? $this->aCurrent['ident'] : 'id', + 'added' => !empty($this->aCurrent['added']) ? $this->aCurrent['added'] : 'added' + ]; + + $this->setConditionParams(); + $aQuery = $this->getSearchDataByParams($aParams); + + /** + * @hooks + * @hookdef hook-simple_search-get_query 'simple_search', 'get_query' - hook on get sql queries + * - $unit_name - equals `simple_search` + * - $action - equals `get_query` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `object` - [array] by ref, array of parameters, can be overridden in hook processing + * - `mode` - [string] search mode + * - `search_object` - [string] search object name + * - `search_query` - [array] by ref, array of sql, can be overridden in hook processing + * @hook @ref hook-simple_search-get_query + */ + bx_alert('simple_search', 'get_query', 0, false, [ + 'object' => &$this->aCurrent, + 'mode' => $this->_sMode, + 'search_object' => $sObject, + 'search_query' => &$aQuery + ]); + + return $aQuery; + } + + function getFieldsOwn() + { + return false; + } + + function getFieldsJoin($sJoin) + { + return false; + } + + /** + * Get array with code for sql elements + * @param $bRenameMode indicator of renmaing fields + * return array with joinFields, ownFields, groupBy and join elements + */ + function getJoins ($bRenameMode = true) + { + if(!isset($this->aCurrent['join']) || !is_array($this->aCurrent['join'])) + return false; + + $aSql = [ + 'join' => '', + 'ownFields' => '', + 'joinFields' => '', + 'groupBy' => '', + 'groupHaving' => '' + ]; + + foreach($this->aCurrent['join'] as $sKey => $aValue) { + $sAlias = isset($aValue['table_alias']) ? $aValue['table_alias'] : $aValue['table']; + $sTableAlias = isset($aValue['table_alias']) ? " AS {$aValue['table_alias']} " : ''; + $sOperator = isset($aValue['operator']) ? $aValue['operator'] : null; + + // joinFields + $aJoinFields = $this->getFieldsJoin($sKey); + if($aJoinFields === false && !empty($aValue['joinFields']) && is_array($aValue['joinFields'])) + $aJoinFields = $aValue['joinFields']; + + if($aJoinFields) + foreach($aJoinFields as $mixedField) { + list($sFldName, $sFldOperator) = is_array($mixedField) ? $mixedField : [$mixedField, $sOperator]; + + $aSql['joinFields'] .= $this->setFieldUnit($sFldName, $sAlias, $sFldOperator, $bRenameMode); + } + + // group by + if(isset($aValue['groupTable'])) + $aSql['groupBy'] .= "`{$aValue['groupTable']}`.`{$aValue['groupField']}`, "; + + // having + if(isset($aValue['groupHaving'])) + $aSql['groupHaving'] .= $aValue['groupHaving']; + + $sOn = isset($aValue['mainTable']) ? $aValue['mainTable'] : $this->aCurrent['table']; + $aSql['join'] .= " {$aValue['type']} JOIN `{$aValue['table']}` $sTableAlias ON " . (!empty($aValue['on_sql']) ? $aValue['on_sql'] : "`{$sAlias}`.`{$aValue['onField']}`=" . trim($this->setFieldUnit($aValue['mainField'], $sOn, isset($aValue['mainFieldFunc']) ? $aValue['mainFieldFunc'] : '', false), ", ")); + $aSql['ownFields'] .= $this->setFieldUnit($aValue['mainField'], $sOn, '', $bRenameMode); + } + + $aSql['joinFields'] = trim($aSql['joinFields'], ', '); + + if(!empty($aSql['groupBy'])) + $aSql['groupBy'] = trim('GROUP BY ' . $aSql['groupBy'], ', '); + + if(!empty($aSql['groupHaving'])) + $aSql['groupHaving'] = 'HAVING ' . $aSql['groupHaving']; + + return $aSql; + } + + /** + * Concat sql parts of query, run it and return result array + * @param $aParams addon param + * return $aData multivariate array + */ + function getSearchDataByParams ($aParams = '') + { + $bForUnion = isset($aParams['for_union']) && $aParams['for_union'] === true; + + $aSql = [ + 'ownFields' => '', + 'order' => '' + ]; + + // searchFields + if($bForUnion) { + $sTable = isset($this->aCurrent['tableSearch']) ? $this->aCurrent['tableSearch'] : $this->aCurrent['table']; + + $aSql['ownFields'] .= $this->setFieldUnit(!empty($this->aCurrent['ident']) ? $this->aCurrent['ident'] : 'id', $sTable); + $aSql['ownFields'] .= $this->setFieldUnit(!empty($this->aCurrent['added']) ? $this->aCurrent['added'] : 'added', $sTable); + $aSql['ownFields'] .= "'" . $this->getContentInfoName() . "' AS `content_info`"; + } + else { + $aOwnFields = $this->getFieldsOwn(); + if($aOwnFields === false) + $aOwnFields = $this->aCurrent['ownFields']; + + foreach($aOwnFields as $mixedField) { + list($sFldName, $sFldOperator) = is_array($mixedField) ? $mixedField : [$mixedField, '']; + + $aSql['ownFields'] .= $this->setFieldUnit($sFldName, $this->aCurrent['table'], $sFldOperator); + } + } + + // joinFields & join + if(($aJoins = $this->getJoins()) !== false) { + if(!$bForUnion) { + $aSql['ownFields'] .= $aJoins['ownFields']; + $aSql['ownFields'] .= $aJoins['joinFields']; + } + + $aSql['join'] = $aJoins['join']; + $aSql['groupBy'] = $aJoins['groupBy']; + $aSql['groupHaving'] = $aJoins['groupHaving']; + } + + $aSql['ownFields'] = trim($aSql['ownFields'], ', '); + + // from + $aSql['from'] = " FROM `{$this->aCurrent['table']}`"; + + // where + $aSql['where'] = $this->getRestriction(); + + // limit + $aSql['limit'] = $this->getLimit(); + + // sorting + $this->setSorting(); + + $aSort = $this->getSorting($this->aCurrent['sorting']); + foreach ($aSort as $sKey => $sValue) + $aSql[$sKey] .= $sValue; + + // execution + $sqlQuery = 'SELECT'; + + if(isset($this->aCurrent['distinct']) && $this->aCurrent['distinct'] === true) + $sqlQuery .= ' DISTINCT'; + + $sqlQuery .= ' ' . $aSql['ownFields']; + + $sqlQuery .= ' ' . $aSql['from']; + + if (isset($aSql['join'])) + $sqlQuery .= ' ' . $aSql['join']; + + if (isset($aSql['where'])) + $sqlQuery .= ' ' . $aSql['where']; + + if (isset($aSql['groupBy'])) + $sqlQuery .= ' ' . $aSql['groupBy']; + + if (isset($aSql['groupHaving'])) + $sqlQuery .= ' ' . $aSql['groupHaving']; + + if($bForUnion) + return [ + 'query' => $sqlQuery, + 'limit' => $aSql['limit'] + ]; + + if (isset($aSql['order'])) + $sqlQuery .= ' ' . $aSql['order']; + + if (isset($aSql['limit'])) + $sqlQuery .= ' ' . $aSql['limit']; + + //echoDbg($sqlQuery); + $aRes = BxDolDb::getInstance()->getAll($sqlQuery); + return $aRes; + } + + /** + * Set class fields condition params and paginate array + */ + function setConditionParams() + { + $this->aCurrent['paginate']['num'] = 0; + + // keyword + $sKeyword = bx_process_input(isset($this->_aCustomSearchCondition['keyword']) ? $this->_aCustomSearchCondition['keyword'] : bx_get('keyword')); + + if ($this->_bLiveSearch && empty($sKeyword)) + return; + + if ($sKeyword !== false) { + if (substr($sKeyword, 0, 1) == '@') { + $sModule = $this->aCurrent['module_name']; + $sMethod = 'act_as_profile'; + if(!bx_is_srv($sModule, $sMethod) || !bx_srv($sModule, $sMethod)) + return; + + $sKeyword = substr($sKeyword, 1); + } + + $this->aCurrent['restriction']['keyword'] = array( + 'value' => $sKeyword, + 'field' => '', + 'operator' => 'against' + ); + + // for search results we need to show all items, not only public content + $this->setProcessPrivateContent(true); + } + + // owner + if (isset($_GET['ownerName'])) { + $sName = bx_process_input($_GET['ownerName']); + $iUser = (int)BxDolProfileQuery::getInstance()->getIdByNickname($sName); + BxDolMenu::getInstance()->setCurrentProfileID($iUser); + } elseif (isset($_GET['userID'])) + $iUser = bx_process_input($_GET['userID'], BX_DATA_INT); + + if (!empty($iUser)) + $this->aCurrent['restriction']['owner']['value'] = $iUser; + + // meta info + if ($this->_sMetaType && !empty($this->aCurrent['object_metatags'])) { + $o = BxDolMetatags::getObjectInstance($this->aCurrent['object_metatags']); + if ($o) { + unset($this->aCurrent['restriction']['keyword']); + switch ($this->_sMetaType) { + case 'location_country': + $o->locationsSetSearchCondition($this, $sKeyword); + break; + case 'location_country_state': + $o->locationsSetSearchCondition($this, $sKeyword, bx_process_input(isset($this->_aCustomSearchCondition['state']) ? $this->_aCustomSearchCondition['state'] : bx_get('state'))); + break; + case 'location_country_city': + $o->locationsSetSearchCondition($this, $sKeyword, bx_process_input(isset($this->_aCustomSearchCondition['state']) ? $this->_aCustomSearchCondition['state'] : bx_get('state')), bx_process_input(isset($this->_aCustomSearchCondition['city']) ? $this->_aCustomSearchCondition['city'] : bx_get('city'))); + break; + case 'location_state': + $o->locationsSetSearchCondition($this, false, $sKeyword); + break; + case 'location_city': + $o->locationsSetSearchCondition($this, false, false, $sKeyword); + break; + case 'mention': + $oCmts = !empty($this->sModuleObjectComments) ? BxDolCmts::getObjectInstance($this->sModuleObjectComments, 0, false) : false; + $o->mentionsSetSearchCondition($this, $sKeyword, $oCmts ? $oCmts->getSystemId() : 0); + break; + case 'keyword': + $oCmts = !empty($this->sModuleObjectComments) ? BxDolCmts::getObjectInstance($this->sModuleObjectComments, 0, false) : false; + $o->keywordsSetSearchCondition($this, $sKeyword, $oCmts ? $oCmts->getSystemId() : 0); + break; + } + } + } + + // category + if ($this->_sCategoryObject){ + if(($o = BxDolCategory::getObjectInstance($this->_sCategoryObject)) && $this->_bSingleSearch) { + if ($this->aCurrent['name'] == $o->getSearchObject()) { + unset($this->aCurrent['restriction']['keyword']); + $o->setSearchCondition($this, $sKeyword); + } + } + if ($this->_sCategoryObject == 'multi'){ + unset($this->aCurrent['restriction']['keyword']); + $this->setCategoriesCondition($sKeyword); + } + } + + $this->setPaginate(); + $iNum = $this->getNum(); + if ($iNum > 0) + $this->aCurrent['paginate']['num'] = $iNum; + } + + /** + * Check number of records on current page + * return number of records on current page + 1 + */ + function getNum () + { + $aJoins = $this->getJoins(false); + $sqlQuery = "SELECT * FROM `{$this->aCurrent['table']}` " . (isset($aJoins['join']) ? ' ' . $aJoins['join'] : '' ) . $this->getRestriction() . (isset($aJoins['groupBy']) ? ' ' . $aJoins['groupBy'] : '') . ' ' . $this->getLimit(true); + return count(BxDolDb::getInstance()->getAll($sqlQuery)); + } + + /** + * Get total number of records + * return total number of records + */ + function getTotal () + { + $aJoins = $this->getJoins(false); + $sqlQuery = "SELECT COUNT(*) FROM `{$this->aCurrent['table']}` " . (isset($aJoins['join']) ? ' ' . $aJoins['join'] : '' ) . $this->getRestriction() . (isset($aJoins['groupBy']) ? ' ' . $aJoins['groupBy'] : ''); + return BxDolDb::getInstance()->getOne($sqlQuery); + } + + /** + * Check restriction params and make condition part of query + * return $sqlWhere sql code of query for WHERE part + */ + function getRestriction () + { + $oDb = BxDolDb::getInstance(); + $sqlWhere = ''; + if (isset($this->aCurrent['restriction'])) { + $aWhere[] = '1'; + foreach ($this->aCurrent['restriction'] as $sKey => $aValue) { + $sqlCondition = ''; + if (isset($aValue['operator']) && isset($aValue['value']) && $aValue['value'] !== '' && $aValue['value'] !== false && $aValue['value'] !== null) { + $sFieldTable = isset($aValue['table']) ? $aValue['table'] : $this->aCurrent['table']; + $sqlCondition = "`{$sFieldTable}`.`{$aValue['field']}` "; + switch ($aValue['operator']) { + case 'empty value': + $sqlCondition .= " = '' "; + break; + case 'not empty value': + $sqlCondition .= " != '' "; + break; + case 'nothing': + $sqlCondition = " 0 "; + break; + case 'against': + $aCond = isset($aValue['field']) && strlen($aValue['field']) > 0 ? $aValue['field'] : $this->aCurrent['searchFields']; + $sqlCondition = !empty($aCond) ? $this->getSearchFieldsCond($aCond, $aValue['value']) : ""; + break; + case 'like': + $sqlCondition .= "LIKE " . $oDb->escape('%' . $aValue['value'] . '%'); + break; + case 'in': + case 'not in': + $sValuesString = $this->getMultiValues($aValue['value']); + $sqlCondition .= strtoupper($aValue['operator']) . '(' . $sValuesString . ')'; + break; + case 'in_set': + $sqlCondition = "1 << (" . $sqlCondition . " - 1) & " . (int)$aValue['value']; + break; + default: + $sqlCondition .= $aValue['operator'] . (isset($aValue['no_quote_value']) && $aValue['no_quote_value'] ? $aValue['value'] : $oDb->escape($aValue['value'])); + break; + } + } + if (strlen($sqlCondition) > 0) + $aWhere[] = $sqlCondition; + } + $sqlWhere .= "WHERE ". implode(' AND ', $aWhere) . (isset($this->aCurrent['restriction_sql']) ? $this->aCurrent['restriction_sql'] : ''); + } + + return $sqlWhere; + } + + /** + * Get limit part of query + * return $sqlFrom code for limit part pf query + */ + function getLimit ($isAddPlusOne = false) + { + if (!isset($this->aCurrent['paginate'])) + return; + + $sqlFrom = (int)$this->aCurrent['paginate']['start'] > 0 ? (int)$this->aCurrent['paginate']['start'] : 0; + $sqlTo = $this->aCurrent['paginate']['perPage'] + ($isAddPlusOne ? 1 : 0); + if ($sqlTo > 0) + return 'LIMIT ' . $sqlFrom . ', ' . $sqlTo; + } + + /** + * Set sorting field of class + */ + function setSorting () + { + $this->aCurrent['sorting'] = isset($_GET[$this->aCurrent['name'] . '_mode']) ? $_GET[$this->aCurrent['name'] . '_mode'] : $this->aCurrent['sorting']; + } + + /** + * Get sorting part of query according current sorting mode + * @param string $sSortType sorting type + * return array with sql elements order and ownFields + */ + function getSorting ($sSortType = 'last') + { + if (isset($this->aCurrent['sorting_sql'])) + return array('order' => $this->aCurrent['sorting_sql']); + + $aOverride = $this->getAlterOrder(); + if (is_array($aOverride) && !empty($aOverride)) + return $aOverride; + + $aSql = array(); + switch ($sSortType) { + case 'rand': + $aSql['order'] = "ORDER BY RAND()"; + break; + case 'score': + if (is_array($this->aCurrent['restriction']['keyword'])) { + $sPseud = ''; + if ('on' == getParam('useLikeOperator')) { + $aSql['order'] = "ORDER BY `score` DESC"; + $sPseud = 'score'; + } + $aSql['ownFields'] .= $this->getSearchFieldsCond($this->aCurrent['searchFields'], $this->aCurrent['restriction']['keyword']['value'], $sPseud) . ','; + } + break; + case 'none': + $aSql['order'] = ""; + break; + default: + $aSql['order'] = "ORDER BY `date` DESC"; + } + return $aSql; + } + + /** + * Return own varaint for sorting (redeclare if necessary) + * return array of sql elements + */ + function getAlterOrder () + { + return array(); + } + + /** + * Set paginate fields of class according GET params 'start' and 'per_page' + * forcePage is need for setting most important number of current page + */ + function setPaginate () + { + if($this->_bValidate) { + $this->aCurrent['paginate']['start'] = 0; + $this->aCurrent['paginate']['perPage'] = count($this->_aParams['validate']); + + return; + } + + $iStart = 0; + $iPerPage = 0; + + //--- Check in aCurrent (low priority). + if(isset($this->aCurrent['paginate']['perPage']) && (int)$this->aCurrent['paginate']['perPage'] != 0) + $iPerPage = (int)$this->aCurrent['paginate']['perPage']; + + //--- Check in aParams (middle priority). + if(isset($this->_aParams['per_page']) && (int)$this->_aParams['per_page'] != 0) + $iPerPage = (int)$this->_aParams['per_page']; + + //--- Check in GET params (high priority). + if(isset($_GET['per_page']) && (int)$_GET['per_page'] != 0) + $iPerPage = (int)$_GET['per_page']; + + //--- Trying to get from System settings. + if (empty($iPerPage)) + $iPerPage = (int)getParam('sys_per_page_search_keyword_' . ($this->_bSingleSearch ? 'single' : 'plural')); + + //--- Use default value in case of emergency. + if (empty($iPerPage)) + $iPerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT; + + if ($this->iPaginatePerPage != BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT) + $iPerPage = $this->iPaginatePerPage; + + $this->aCurrent['paginate']['perPage'] = $iPerPage; + + if(!isset($this->aCurrent['paginate']['forceStart'])) { + if(isset($this->_aParams['start'])) + $iStart = (int)$this->_aParams['start']; + + if(($iStartGet = bx_get('start')) !== false) + $iStart = (int)$iStartGet; + } + else + $iStart = (int)$this->aCurrent['paginate']['forceStart']; + + if($iStart < 0) + $iStart = 0; + + $this->aCurrent['paginate']['start'] = $iStart; + } + + function unsetPaginate() + { + unset($this->aCurrent['paginate']); + } + + /** + * Get sql where condition for search fields + * @param array of search fields + * @param string $sKeyword keyword value for search + * @param string sPseud for setting new name for generated set of fields in query + * return sql code of WHERE part in query + */ + function getSearchFieldsCond ($aFields, $sKeyword, $sPseud = '') + { + if (!$sKeyword) + return ''; + + $sTable = empty($this->aCurrent['tableSearch']) ? $this->aCurrent['table'] : $this->aCurrent['tableSearch']; + + $oDb = BxDolDb::getInstance(); + + $bLike = getParam('useLikeOperator'); + + if (!is_array($aFields)) + $aFields = array($aFields); + + if ($bLike == 'on') { + $sKeyword = $oDb->escape('%' . preg_replace('/\s+/', '%', $sKeyword) . '%'); + + $sSqlWhere = ''; + foreach ($aFields as $sValue) + $sSqlWhere .= "`{$sTable}`.`$sValue` LIKE " . $sKeyword . " OR "; + + $sSqlWhere = '(' . trim($sSqlWhere, 'OR ') . ')'; + + } else { + $sKeyword = $oDb->escape($sKeyword); + + $sSqlWhere = ''; + foreach ($aFields as $sValue) + $sSqlWhere .= "`{$sTable}`.`$sValue`, "; + + $sSqlWhere = trim($sSqlWhere, ', '); + $sSqlWhere = " MATCH({$sSqlWhere}) AGAINST (" . $sKeyword . ") "; + + if (!empty($sPseud)) + $sSqlWhere .= " AS `$sPseud` "; + } + + return $sSqlWhere; + } + + /** + * Get set from several values for 'in' and 'not in' operators + * @param $aValues array of values + * return sql code for field with operator IN (NOT IN) + */ + function getMultiValues ($aValues) + { + $oDb = BxDolDb::getInstance(); + return $oDb->implode_escape($aValues); + } + + /** + * System method for filling aPseud array. Fill field aPseud for current class from system option. + * It may be useful when Fields are taken from system option too. + */ + function _getPseudFromParam () + { + return false; + } + + /** + * System method for filling aPseud array. + * Fill field aPseud for current class (if you will use own getSearchUnit methods then not necessary to redeclare). + */ + function _getPseud () + { + + } + + /** + * Add replace markers. Markers are replaced in titles and browse urls + * @param $a array of markers as key => value + * @return true on success or false on error + */ + public function addMarkers ($a) + { + if (empty($a) || !is_array($a)) + return false; + $this->_aMarkers = array_merge ($this->_aMarkers, $a); + return true; + } + + /** + * Replace provided markers in a string + * @param $mixed string or array to replace markers in + * @return string where all occured markers are replaced + */ + protected function _replaceMarkers ($mixed) + { + return bx_replace_markers($mixed, $this->_aMarkers); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSockets.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSockets.php new file mode 100644 index 0000000000..077dbb5c99 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolSockets.php @@ -0,0 +1,70 @@ +_sIsEnabled = false; + + if(getParam('sys_sockets_type') == 'sys_sockets_disabled') + return; + + $sUrl = trim(getParam('sys_sockets_url')); + if(!$sUrl) + return; + + $a = parse_url($sUrl); + $this->_sHost = $a['host']; + $this->_sPort = $a['port']; + $this->_sScheme = $a['scheme']; + + $this->_sIsEnabled = true; + } + + static public function getInstance() + { + if(!isset($GLOBALS['bxDolClasses']['BxDolSockets'])){ + $GLOBALS['bxDolClasses']['BxDolSockets'] = new BxDolSockets(); + if (getParam('sys_sockets_type') == 'sys_sockets_soketi') + $GLOBALS['bxDolClasses']['BxDolSockets'] = new BxDolSocketsSoketi(); + } + + return $GLOBALS['bxDolClasses']['BxDolSockets']; + } + + public function isEnabled() + { + return $this->_sIsEnabled; + } + + public function sendEvent($sSocket, $iContentId, $sEvent, $sMessage) + { + return; + } + + public function getJsCode() + { + return ''; + } + + public function writeLog($sString) + { + bx_log('sys_sockets', $sString); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorage.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorage.php new file mode 100644 index 0000000000..a25a18d32a --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorage.php @@ -0,0 +1,1575 @@ + + * Choose a file to upload: + * + *
+ * + * + * @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: ;"; + * @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 + protected $_aMimeTypesViewable = ['audio/', 'image/', 'video/']; ///< file types (by mime type) to allow view file in browser instead of downloading + + /** + * 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; + + $aExtMarkers = [ + 'image' => getParam('sys_files_ext_images'), + 'video' => getParam('sys_files_ext_video'), + 'imagevideo' => getParam('sys_files_ext_imagevideo'), + 'audio' => getParam('sys_files_ext_audio'), + 'dangerous' => getParam('sys_files_ext_dangerous') + ]; + + $aObject['ext_allow'] = bx_replace_markers($aObject['ext_allow'], $aExtMarkers); + $aObject['ext_deny'] = bx_replace_markers($aObject['ext_deny'], $aExtMarkers); + + $sClass = 'BxDolStorage' . $aObject['engine']; + $o = new $sClass($aObject); + + if (!$o->isInstalled() || !$o->isAvailable()) + return false; + + return ($GLOBALS['bxDolClasses']['BxDolStorage!'.$sObject] = $o); + } + + /** + * Delete old security tokens from database. + * It is alutomatically called upin cron execution, usually once in a day. + * @return number of deleted records + */ + public static function pruning() + { + $iDeleted = 0; + $a = BxDolStorageQuery::getStorageObjects(); + foreach ($a as $aObject) { + $oDb = new BxDolStorageQuery($aObject); + $iDeleted += $oDb->prune(); + } + return $iDeleted; + } + + /** + * Delete files queued for deletions + * It is alutomatically called upin cron execution, usually one time per minute. + * Max number of deletetion per time is defined in @see BX_DOL_STORAGE_QUEUED_DELETIONS_PER_RUN + * @return number of deleted records + */ + public static function pruneDeletions() + { + $iDeleted = 0; + $a = BxDolStorageQuery::getQueuedFilesForDeletion(BX_DOL_STORAGE_QUEUED_DELETIONS_PER_RUN); + foreach ($a as $r) { + $o = BxDolStorage::getObjectInstance($r['object']); + $iDeleted += ($o && $o->deleteFile($r['file_id']) ? 1 : 0); + } + + return $iDeleted; + } + + /** + * Check if module has any files pending for deletion, it is supposed that all module storage object names are prefixed with module name + * @param $sPrefix - usually module name + * @return number of files pending for deletion which were found by prefix + */ + public static function isQueuedFilesForDeletion ($sPrefix) + { + return BxDolStorageQuery::isQueuedFilesForDeletion($sPrefix); + } + + /** + * Get file token for private files. + * @param $iFileId file + * @return file token string + */ + public function genToken($iFileId) + { + return $this->_oDb->genToken($iFileId); + } + + /** + * Change storage engine. It's possible to change it when there is no files in storage engine. + * @param $sEngine new storage engine + * @return true on success or false on error + */ + public function changeStorageEngine ($sEngine) + { + if (0 == $this->_aObject['current_size'] && 0 == $this->_aObject['current_number']) + return $this->_oDb->changeStorageEngine($sEngine); + return false; + } + + /** + * Is storage engine available? + * @return boolean + */ + function isAvailable() + { + return true; + } + + /** + * Are required php modules installed for this storage engine ? + * @return boolean + */ + public function isInstalled() + { + return true; + } + + public function getObject() + { + return $this->_aObject['object']; + } + + public function getObjectData() + { + return $this->_aObject; + } + + /** + * Get error code from the last occured error + * @return error code + */ + public function getErrorCode() + { + return $this->_iErrorCode; + } + + /** + * Get error string from the last occured error + * @return error string + */ + public function getErrorString() + { + bx_import('BxDolLanguages'); + $a = array ( + 1000 => '_sys_storage_err_no_input_method', + 1001 => '_sys_storage_err_no_file', + 1002 => '_sys_storage_invalid_file', + 1003 => '_sys_storage_err_file_too_big', + 1004 => '_sys_storage_err_wrong_ext', + 1005 => '_sys_storage_err_user_quota_exceeded', + 1006 => '_sys_storage_err_object_quota_exceeded', + 1007 => '_sys_storage_err_site_quota_exceeded', + 1008 => '_sys_storage_err_engine_add', + + 2001 => '_sys_storage_err_file_not_found', + 2002 => '_sys_storage_err_unlink', + + 5001 => '_sys_storage_err_db', + 5002 => '_sys_storage_err_filesystem_perm', + 5003 => '_sys_storage_err_permission_denied', + 5004 => '_sys_storage_err_engine_get', + 5005 => '_sys_storage_err_not_implemented', + ); + return _t($a[$this->_iErrorCode]); + } + + /** + * Get max file size allowed for current user, it checks user quota, object quota, site quota and php setting + * @param $iProfileId profile id to check quota for + * @return quota size in bytes + */ + public function getMaxUploadFileSize ($iProfileId) + { + $iMin = PHP_INT_MAX; + + $aUserQuota = $this->_oDb->getUserQuota($iProfileId); + if ($aUserQuota['max_file_size'] && $aUserQuota['max_file_size'] < $iMin) + $iMin = $aUserQuota['max_file_size']; + + $aObjectQuota = $this->_oDb->getStorageObjectQuota(); + if ($aObjectQuota['max_file_size'] && $aObjectQuota['max_file_size'] < $iMin) + $iMin = $aObjectQuota['max_file_size']; + + // TODO: get and check site quota + + if (!defined('BX_DOL_CRON_EXECUTE')) { + $iUploadMaxFilesize = return_bytes(ini_get('upload_max_filesize')); + if ($iUploadMaxFilesize && $iUploadMaxFilesize < $iMin) + $iMin = $iUploadMaxFilesize; + } + + return $iMin; + } + + /** + * Store file in the storage area. It is not recommended to use this function directly, + * use other funtions like: storeFileFromForm, storeFileFromXhr, storeFileFromPath, storeFileFromUrl + * @param $sMethod upload method, like regular Form upload, upload from URL, etc + * @param $aMethodParams upload method params + * @param $sName file name with extention + * @param $isPrivate private or public file + * @param $iProfileId profile id of the upload action performer + * @param $iContentId content id to associate with ghost file + * @return id of added file on success, false on error - to get exact error string call getErrorString() + */ + public function storeFile($sMethod, $aMethodParams, $sName = false, $isPrivate = true, $iProfileId = 0, $iContentId = 0) + { + // setup input source using helper classes, like $_FILES or some URL for example + + $sHelperClass = 'BxDolStorageHelper' . $sMethod; + if (!class_exists($sHelperClass)) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_NO_INPUT_METHOD); + return false; + } + + $oHelper = new $sHelperClass($aMethodParams); + + // check for errors, like size and extentions checking + + if ($iImmediateError = $oHelper->getImmediateError()) { + $this->setErrorCode($iImmediateError); + return false; + } + + $sExt = $this->getFileExt($oHelper->getName()); + $sMimeType = $this->getMimeTypeByFileName($oHelper->getName()); + + if (!$this->isValidExt($sExt)) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_WRONG_EXT); + return false; + } + + // before upload callback + additional checking + + if (!$this->onBeforeFileAdd (array( + 'profile_id' => $iProfileId, + 'content_id' => $iContentId, + 'file_name' => $oHelper->getName(), + 'mime_type' => $sMimeType, + 'ext' => $sExt, + 'size' => $oHelper->getSize(), + 'private' => $isPrivate ? 1 : 0, + ))) { + return false; + } + + // create tmp file + + $sTmpFile = tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']); + if (!$oHelper->save($sTmpFile)) { + $this->setErrorCode(BX_DOL_STORAGE_INVALID_FILE); + return false; + } + + // process additional custom fields, like video duration and video dimension + + $aAdditionalFields = array(); + if (isset($this->_aParams['fields']) && is_array($this->_aParams['fields'])) { + foreach ($this->_aParams['fields'] as $sField => $mixedMethod) { + if (is_string($mixedMethod) && method_exists($this, $mixedMethod)) { + $aAdditionalFields[$sField] = $this->$mixedMethod($sTmpFile, $sMimeType, $sExt, $this); + } + elseif (is_array($mixedMethod) && isset($mixedMethod['module']) && isset($mixedMethod['method'])) { + $mixedMethod['params'] = array($sTmpFile, $sMimeType, $sExt, $this); + $aAdditionalFields[$sField] = call_user_func_array('bx_srv', array($mixedMethod['module'], $mixedMethod['method'], $mixedMethod['params'], isset($mixedMethod['class']) ? $mixedMethod['class'] : 'Module')); + } + } + } + + bx_alert('system', 'store_file', $iContentId, $iProfileId, array( + 'storage_object' => $this->_aObject['object'], + 'object' => $this, + 'file_name' => $oHelper->getName(), + 'mime_type' => $sMimeType, + 'ext' => $sExt, + 'size' => $oHelper->getSize(), + 'file_path' => $sTmpFile, + 'store_method' => $sMethod, + 'store_method_params' => $aMethodParams, + )); + + // 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, $aAdditionalFields); + $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. + * @param boolean $bForceDownloadDialog if downloading to a local file system first is required and/or send the outout as attachment rather than inline. + */ + public function download ($aFile, $sToken = false, $bForceDownloadDialog = 'auto') + { + $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 $sRemoteId file remote id + * @return file url or false on error + */ + public function getFileUrlByRemoteId($sRemoteId) + { + $aFile = $this->_oDb->getFileByRemoteId($sRemoteId); + if (!$aFile) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND); + return false; + } + + return $this->getFileUrlById($aFile['id']); + } + + /** + * 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) + { + $a = $this->_oDb->getFileById($iFileId); + if (!$a) return false; + + // update custom fields for previously uploaded files + if (!defined('BX_DOL_STORAGE_CUSTOM_FIELDS_UPDATE_SKIP') && isset($this->_aParams['fields']) && is_array($this->_aParams['fields'])) { + foreach ($this->_aParams['fields'] as $sField => $mixedMethod) { + if ($a[$sField]) + continue; + + $sFileUrl = $this->getFileUrlById($iFileId); + + if (is_string($mixedMethod) && method_exists($this, $mixedMethod)) { + $a[$sField] = $this->$mixedMethod($sFileUrl, $a['mime_type'], $a['ext'], $this); + } + elseif (is_array($mixedMethod) && isset($mixedMethod['module']) && isset($mixedMethod['method'])) { + $mixedMethod['params'] = array($sFileUrl, $a['mime_type'], $a['ext'], $this); + $a[$sField] = call_user_func_array('bx_srv', array($mixedMethod['module'], $mixedMethod['method'], $mixedMethod['params'], isset($mixedMethod['class']) ? $mixedMethod['class'] : 'Module')); + } + if ($a[$sField]) + $this->_oDb->modifyCustomField($iFileId, $sField, $a[$sField], false); + } + } + + return $a; + } + + /** + * Get ghost file info array by file id. + * @param $iFileId file id + * @return array + */ + public function getGhost($iFileId) + { + return $this->_oDb->getGhost($iFileId); + } + + /** + * check if file is private or public + * @param $iFileId file id + * @return boolean + */ + public function isFilePrivate($iFileId) + { + $aFile = $this->getFile ($iFileId); + return $aFile['private'] ? true : false; + } + + /** + * Call this function after saving/associate just uploaded file id, so file is not orphaned/ghost. + * Ghost files appear on download form automaticaly during next upload, + * for example if file was uploaded but was not submitted for some reason. + * This mechanism ensure that the file is not lost. + * @param $mixedFileIds array of file ids or just one file id + * @param $iProfileId profile id + * @param return number of deleted ghost files + */ + public function afterUploadCleanup($mixedFileIds, $iProfileId, $iContentId = false) + { + return $this->_oDb->deleteGhosts($mixedFileIds, $iProfileId, $iContentId); + } + + /** + * Get ghost/orphaned files for particular user. + * @param $iProfileId profile id + * @param $iContentId content id, or false to not consider content id at all + * @param $isCheckAllAccountProfiles get all files associated with all account profiles + * @param $isAdmin if true, then don't check files ownership, it makes sense when $iContentId is provided, so it will return all files assiciated with content + * @return array of arrays + */ + public function getGhosts($iProfileId, $iContentId = false, $isCheckAllAccountProfiles = false, $isAdmin = false) + { + if ($isCheckAllAccountProfiles && ($oProfile = BxDolProfile::getInstance($iProfileId))) { + $oAccount = $oProfile->getAccountObject(); + $aProfiles = $oAccount->getProfilesIds(false, false); + return $this->_oDb->getGhosts($aProfiles, $iContentId, $isAdmin); + } + + return $this->_oDb->getGhosts($iProfileId, $iContentId, $isAdmin); + } + + /** + * Reorder ghost/orphaned files for particular content/user. + * @param $iProfileId profile id + * @param $iContentId content id, or false to not consider content id at all + * @param $aGhosts an ordered list of ghost/orphaned files' IDs. + * @return boolean result of operation + */ + public function reorderGhosts($iProfileId, $iContentId, $aGhosts) + { + $bResult = true; + + $iGhosts = count($aGhosts); + for($i = 0; $i < $iGhosts; $i++) + $bResult &= $this->_oDb->updateGhostOrder($iProfileId, $iContentId, (int)$aGhosts[$i], $i); + + return $bResult; + } + + /** + * Update ghosts' content id. + * @param $mixedFileIds array of file ids or just one file id + * @param $iProfileId profile id + * @param $iContentId content id + * @param $isAdmin if true, then don't check files ownership + * @return true on success or false otherwise + */ + public function updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId, $isAdmin = false) + { + $aProfiles = array(); + if ($oProfile = BxDolProfile::getInstance($iProfileId)) { + $oAccount = $oProfile->getAccountObject(); + $aProfiles = $oAccount->getProfilesIds(false); + } + + return $this->_oDb->updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId, $aProfiles, $isAdmin); + } + + /** + * Get files list for particular user. + * @param $iProfileId profile id + * @return array of arrays + */ + public function getFiles($iProfileId) + { + return $this->_oDb->getFiles($iProfileId); + } + + /** + * Get all files in the storage + */ + public function getFilesAll($iStart = 0, $iPerPage = 1000) + { + return $this->_oDb->getFilesAll($iStart, $iPerPage); + } + + /** + * Get readable representation of restrictions by file extentions. + * @param $iProfileId profile id + * @return string + */ + public function getRestrictionsTextExtensions ($iProfileId) + { + switch ($this->_aObject['ext_mode']) { + case 'allow-deny': + if (!$this->_aObject['ext_allow']) + return _t('_sys_storage_restriction_ext_all_denied'); + return _t('_sys_storage_restriction_ext_allowed', _t_format_extensions($this->_aObject['ext_allow'])); + case 'deny-allow': + if (!$this->_aObject['ext_deny']) + return _t('_sys_storage_restriction_ext_all_allowed'); + return _t('_sys_storage_restriction_ext_denied', _t_format_extensions($this->_aObject['ext_deny'])); + default: + return _t('_sys_storage_restriction_ext_all_denied'); + } + } + + /** + * @return list of allowed extensions, if ext_mode = 'allow-deny', returns false otherwise. + */ + public function getAllowedExtensions () + { + if ('allow-deny' == $this->_aObject['ext_mode']) + return _t_format_extensions($this->_aObject['ext_allow']); + return null; + } + + /** + * Get readable representation of restrictions by file size. + * @param $iProfileId profile id + * @return string + */ + public function getRestrictionsTextFileSize ($iProfileId) + { + return _t('_sys_storage_restriction_size', _t_format_size($this->getMaxUploadFileSize($iProfileId))); + } + + /** + * Get readable representation of all restrictions. + * @param $iProfileId profile id + * @return array of strings + */ + public function getRestrictionsTextArray ($iProfileId) + { + $aTypes = array('Extensions', 'FileSize'); + $aRet = array(); + foreach ($aTypes as $sType) { + $sFunc = 'getRestrictionsText' . $sType; + $s = $this->$sFunc ($iProfileId); + if ($s) + $aRet[] = $s; + } + return $aRet; + } + + /** + * Reread available mimetypes from particular file. + * It clears 'sys_storage_mime_types' table and fill it with data form provided file. + * The format of file is: mime/type _space_or_tab_ extentions_sperated_by_space. + * Usually the file is mime.types file from apache or /etc/mime.types from unix systems. + * @param $sFile file to read mime types from + * @return false if file was not found or can not be read, string with result on other case - it can contains file markup errors or localized "Success" string if everything went fine. + */ + public function reloadMimeTypesFromFile ($sFile) + { + $sResult = ''; + + /* + * These mime types must be manually added/replaced if they aren't defined in the mime type file + * + + text/x-php php + text/x-coffeescript coffee + text/x-common-lisp lsp lisp + text/x-diff diff + text/x-go go + text/x-java java + text/x-lua lua + text/x-perl pl prl perl + text/x-python py + text/nginx nginx + text/x-ini ini + text/x-ruby rb + text/x-sass sass + text/x-sh bash sh + text/x-swift swift + text/x-vb vb + text/vbscript vbs + text/x-vue vue + text/x-yaml yaml + text/x-sql sql + text/x-markdown md + application/xquery xq xquery + application/x-powershell ps1 + application/x-aspx aps + application/x-jsp jsp + + */ + + $aIconsFont = array ( + 'far file-pdf' => array('pdf'), + 'far file-word' => array('doc', 'docx'), + 'far file-excel' => array('xls', 'xlt', 'xlsx', 'sxc', 'stc', 'ods', 'ots', 'sdc', 'csv', 'dif', 'slk', 'pxl'), + 'far file-powerpoint' => array('ppt', 'pptx', 'sxi', 'sti', 'odp', 'sdp', 'sdd'), + 'far file-code' => array('1st', 'aspx', 'asp', 'json', 'js', 'jsp', 'java', 'php', 'xml', 'html', 'xhtml', 'htm', 'rdf', 'xsd', 'xsl', 'xslt', 'sax', 'rss', 'dtd', 'cfm', 'js', 'asm', 'pl', 'prl', 'bas', 'b', 'fs', 'src', 'cs', 'ws', 'cgi', 'bat', 'py', 'c', 'cpp', 'cc', 'cp', 'h', 'hh', 'cxx', 'hxx', 'c++', 'm', 'lua', 'swift', 'sh', 'as', 'cob', 'tpl', 'lsp', 'x', 'cmd', 'rb', 'cbl', 'pas', 'pp', 'vb', 'vbs', 'f', 'perl', 'jl', 'lol', 'bal', 'pli', 'css', 'less', 'sass', 'saas', 'scss', 'bcc', 'coffee', 'jade', 'j', 'tea', 'c#', 'sas', 'diff', 'pro', 'for', 'sh', 'bsh', 'bash', 'twig', 'csh', 'lisp', 'lsp', 'cobol', 'pl', 'd', 'git', 'rb', 'hrl', 'cr', 'inp', 'a', 'go', 'as3', 'm', 'sql', 'md', 'mbox', 'nginx', 'pgp', 'asc', 'sig', 'ps1', 'rq', 'ttl', 'vue', 'xquery', 'xq'), + 'far file-image' => 'image/', + 'far file-video' => 'video/', + 'far file-audio' => 'audio/', + 'far file-alt' => 'text/', + 'far file-archive' => array('7z', '7zip', 'aar', 'ace', 'alz', 'arj', 'bz2', 'bza', 'bzip2', 'bzp', 'bzp2', 'cab', 'czip', 'gnutar', 'gz', 'gza', 'gzi', 'gzip', 'ha', 'lhz', 'lzma', 'pzip', 'rar', 'roo', 's7z', 'tar', 'tar-gz', 'tar-lzma', 'tar-z', 'taz', 'tbz', 'tbz2', 'tgz', 'tz', 'z', 'zip', 'zipx', 'zix', 'zoo'), + ); + + $aIcons = array ( + 'mime-type-psd.svg' => array('psd'), + 'mime-type-png.svg' => array('png'), + 'mime-type-image.svg' => 'image/', + 'mime-type-video.svg' => 'video/', + 'mime-type-audio.svg' => 'audio/', + 'mime-type-presentation.svg' => array('ppt', 'pptx', 'sxi', 'sti', 'odp', 'sdp', 'sdd'), + 'mime-type-spreadsheet.svg' => array('xls', 'xlt', 'xlsx', 'sxc', 'stc', 'ods', 'ots', 'sdc', 'csv', 'dif', 'slk', 'pxl'), + 'mime-type-document.svg' => array('doc', 'docx', 'odt', 'ott', 'sxw', 'stw', 'rtf', 'sdw', 'txt', 'pdb', 'psw', 'pdf'), + 'mime-type-vector.svg' => 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.svg' => 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]); + } + + public function queueFiles($aFiles) + { + if (!$aFiles) + return 0; + + $a = array(); + foreach ($aFiles as $aFile) + $a[] = $aFile['id']; + + return $this->queueFilesForDeletion($a); + } + + protected function getFileDuration($sFilePath, $sMimeType, $sExt, $oStorage) + { + if (strncmp('video/', $sMimeType, 6) === 0) + return (int)BxDolTranscoderVideo::getDuration($sFilePath); + return 0; + } + + protected function getFileDimensions($sFilePath, $sMimeType, $sExt, $oStorage) + { + if (strncmp('image/', $sMimeType, 6) === 0 && $o = BxDolTranscoderImage::getObjectAbstract()) { + $a = $o->getSize($sFilePath); + if ($a && isset($a['w']) && isset($a['h'])) + return $a['w'] . 'x' . $a['h']; + } + + if (strncmp('video/', $sMimeType, 6) === 0 && $o = BxDolTranscoderVideo::getObjectAbstract()) { + $a = $o->getSize($sFilePath); + if ($a && isset($a['w']) && isset($a['h'])) + return $a['w'] . 'x' . $a['h']; + } + return ''; + } + + protected function isAuthUrl ($aFile) + { + $s = trim(getParam('sys_storage_s3_force_auth_urls')); + if ('*' == $s) + return true; + + if ($s && ($aStorages = explode(',', $s))) + return in_array($this->getObject(), $aStorages); + + return $aFile['private']; + } +} + + +/** + * Handle file uploads via XMLHttpRequest + */ +class BxDolStorageHelperXhr +{ + protected $sName; + + function __construct ($aParams) + { + $this->sName = $aParams['name']; + } + + function getImmediateError() + { + return BX_DOL_STORAGE_ERR_OK; + } + + function save($path) + { + $input = fopen("php://input", "r"); + $temp = tmpfile(); + $realSize = stream_copy_to_stream($input, $temp); + fclose($input); + + if (false === $this->getSize() || $realSize != $this->getSize()) + return false; + + $target = fopen($path, "w"); + fseek($temp, 0, SEEK_SET); + stream_copy_to_stream($temp, $target); + fclose($target); + fclose($temp); + + return true; + } + + function getName() + { + return $this->sName; + } + + function getSize() + { + if (isset($_SERVER["CONTENT_LENGTH"])) + return (int)$_SERVER["CONTENT_LENGTH"]; + else + return false; + } +} + +/** + * Handle file uploads via regular form post (uses the $_FILES array) + */ +class BxDolStorageHelperForm +{ + protected $aFile; + + function __construct ($aParams) + { + $this->aFile = $aParams['file']; + } + + function getImmediateError() + { + if (!$this->aFile['size'] || !$this->aFile['tmp_name']) + return BX_DOL_STORAGE_ERR_NO_FILE; + + if (UPLOAD_ERR_OK != $this->aFile['error']) + return (int)$this->aFile['error']; + + return BX_DOL_STORAGE_ERR_OK; + } + + function save($path) + { + if (!move_uploaded_file($this->aFile['tmp_name'], $path)){ + return false; + } + return true; + } + + function getName() + { + return $this->aFile['name']; + } + + function getSize() + { + return $this->aFile['size']; + } +} + +/** + * Store file from local file path + */ +class BxDolStorageHelperPath +{ + protected $sPath; + + function __construct ($aParams) + { + $this->sPath = $aParams['path']; + } + + function getImmediateError() + { + if (!$this->sPath || !file_exists($this->sPath)) + return BX_DOL_STORAGE_ERR_NO_FILE; + + return BX_DOL_STORAGE_ERR_OK; + } + + function save($path) + { + if (!copy($this->sPath, $path)) { + return false; + } + return true; + } + + function getName() + { + return pathinfo($this->sPath, PATHINFO_BASENAME); + } + + function getSize() + { + return filesize($this->sPath); + } +} + +/** + * Store file from URL + */ +class BxDolStorageHelperUrl extends BxDolStorageHelperPath +{ + protected $aMime2Ext = array ( + 'image/bmp' => 'bmp', + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg', + 'image/pjpeg' => 'jpg', + 'image/png' => 'png', + ); + + function __construct ($aParams) + { + $aParams['path'] = ''; + $sExt = pathinfo(parse_url($aParams['url'], PHP_URL_PATH), PATHINFO_EXTENSION); + if ($sTmpFilename = tempnam(BX_DIRECTORY_PATH_TMP, '')) { + $s = ''; + if (!$sExt) { + $s = @file_get_contents($aParams['url']); + $oFinfo = new finfo(FILEINFO_MIME_TYPE); + $sMime = $oFinfo->buffer($s); + if (isset($this->aMime2Ext[$sMime])) + $sExt = $this->aMime2Ext[$sMime]; + } + + $aParams['path'] = $sTmpFilename . '.' . $sExt; + @rename($sTmpFilename, $aParams['path']); + + if ($s) { + file_put_contents($aParams['path'], $s); + } + else { + $hRead = @fopen($aParams['url'], "rb"); + $hWrite = fopen($aParams['path'], "wb"); + if (false !== $hRead && false !== $hWrite) { + while (!feof($hRead)) { + $data = fread($hRead, 8192); + if (false === fwrite($hWrite, $data)) { + fclose($hWrite); + file_put_contents($aParams['path'], ''); + break; + } + } + fclose($hRead); + fclose($hWrite); + } + } + } + parent::__construct($aParams); + } + + function getImmediateError() + { + $iRet = parent::getImmediateError(); + if (BX_DOL_STORAGE_ERR_OK != $iRet) + return $iRet; + return $this->getSize() > 0 ? BX_DOL_STORAGE_ERR_OK : BX_DOL_STORAGE_INVALID_FILE; + } + + function __destruct() + { + @unlink($this->sPath); + } +} + +/** + * Handle file uploads from the same or another storage object + */ +class BxDolStorageHelperStorage +{ + protected $iFileId; + protected $oStorage; + protected $aFile; + + function __construct ($aParams) + { + $this->iFileId = $aParams['id']; + $this->oStorage = BxDolStorage::getObjectInstance($aParams['storage']); + + $this->aFile = false; + if ($this->oStorage) + $this->aFile = $this->oStorage->getFile($this->iFileId); + } + + function getImmediateError() + { + if (!$this->iFileId) + return BX_DOL_STORAGE_ERR_NO_FILE; + + if (!$this->oStorage) + return BX_DOL_STORAGE_ERR_ENGINE_GET; + + if (!$this->aFile) + return BX_DOL_STORAGE_ERR_NO_FILE; + + return BX_DOL_STORAGE_ERR_OK; + } + + function save($path) + { + $s = bx_file_get_contents ($this->oStorage->getFileUrlById($this->iFileId)); + if (!$s) + return false; + + return file_put_contents($path, $s) ? true : false; + } + + function getName() + { + return $this->aFile['file_name']; + } + + function getSize() + { + return $this->aFile['size']; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3.php new file mode 100644 index 0000000000..c7d2d88c8e --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3.php @@ -0,0 +1,287 @@ +_bSSL = (isset($this->_aParams['ssl']) && $this->_aParams['ssl']) || (!isset($this->_aParams['ssl']) && strncmp(BX_DOL_URL_ROOT, 'https://', 8) === 0) ? true : false; + + $this->_sEndpoint = getParam('sys_storage_s3_endpoint'); + $this->_sBucket = getParam('sys_storage_s3_bucket'); + $this->_sDomain = getParam('sys_storage_s3_domain'); + $this->_bReducedRedundancy = isset($this->_aParams['reduced_redundancy']) && $this->_aParams['reduced_redundancy'] ? true : false; + + $this->init($aObject); + } + + protected function init ($aObject) + { + require_once(BX_DIRECTORY_PATH_PLUGINS . 'unaio/amazon-s3-php-class-hmac-v2/S3.php'); + $this->_s3 = new S3( + getParam('sys_storage_s3_access_key'), + getParam('sys_storage_s3_secret_key'), + $this->_bSSL, + $this->_sEndpoint ? $this->_sEndpoint : 's3.amazonaws.com' + ); + if ($this->_bSSL && getParam('sys_curl_ssl_allow_untrusted')) + $this->_s3->setSSL($this->_bSSL, false); + $this->_s3->setExceptions(true); + } + + /** + * Get file url. + * @param $iFileId file + * @return file url + */ + public function getFileUrlById($iFileId) + { + $aFile = $this->_oDb->getFileById($iFileId); + if (!$aFile) + return false; + + if ($this->isAuthUrl($aFile)) { + $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 downloading by remote id. If file is private then token is checked. + */ + public function download ($sRemoteId, $sToken = false, $bForceDownloadDialog = 'auto') + { + $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']); + + if ($bForceDownloadDialog && $bForceDownloadDialog !== 'auto') { + // download remote file to tmp + $sTmpFilePath = BX_DIRECTORY_PATH_TMP . 'dwnld_'.$sRemoteId; + if (!file_exists($sTmpFilePath)) { + @file_put_contents($sTmpFilePath, bx_file_get_contents($sUrl)); + } + + // read from a local storage to be able to send it as attachment and give it a proper name + if (!file_exists($sTmpFilePath) || !bx_smart_readfile($sTmpFilePath, $aFile['file_name'], $aFile['mime_type'], $aFile['private'] && $this->_iCacheControl > $this->_aObject['token_life'] ? $this->_aObject['token_life'] : $this->_iCacheControl, $aFile['private'] ? 'private' : 'public', 'attachment')) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET); + return false; + } + } else { + header("Location: " . $sUrl); + } + + return parent::download($aFile); + } + + /** + * Set file private or public. + */ + public function setFilePrivate($iFileId, $isPrivate = true) + { + $aFile = $this->_oDb->getFileById($iFileId); + if (!$aFile) + return false; + + try { + $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; + } + } + } catch (Exception $e) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET); + bx_log('sys_storage_s3', $e->getMessage()); + return false; + } + + return parent::setFilePrivate($iFileId, $isPrivate); + } + + // ---------------- + + protected function generateHeaders($sFileName, $isPrivate, $sMimeType = '') + { + if (!$sMimeType) + $sMimeType = $this->getMimeTypeByFileName($sFileName); + + $aRequestHeaders = array ( + "Content-Type" => $sMimeType, + ); + if ($this->_iCacheControl > 0) { + $aRequestHeaders = array_merge ($aRequestHeaders, array ( + "Cache-Control" => "max-age=" . ($isPrivate && $this->_iCacheControl > $this->_aObject['token_life'] ? $this->_aObject['token_life'] : $this->_iCacheControl), + )); + } + + return $aRequestHeaders; + } + + protected function addFileToEngine($sTmpFile, $sLocalId, $sName, $isPrivate, $iProfileId) + { + try { + $sMimeType = $this->getMimeTypeByFileName($sName); + $sExt = $this->getFileExt($sName); + $sPath = $this->genPath($sLocalId, $this->_aObject['levels']); + $sRemoteNamePath = $sPath . $sLocalId . ($sExt ? '.' . $sExt : ''); + $aRequestHeaders = $this->generateHeaders($sName, $isPrivate); + $aMetaHeaders = array(); + + $sStorageClass = $this->_bReducedRedundancy ? S3::STORAGE_CLASS_RRS : S3::STORAGE_CLASS_STANDARD; + $sACL = $isPrivate ? S3::ACL_AUTHENTICATED_READ : S3::ACL_PUBLIC_READ; + $aInputFile = $this->_s3->inputFile($sTmpFile); + if (!$this->_s3->putObject($aInputFile, $this->_sBucket, $this->getObjectBaseDir($isPrivate) . $sRemoteNamePath, $sACL, $aMetaHeaders, $aRequestHeaders, $sStorageClass)) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD); + return false; + } + } catch (Exception $e) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD); + bx_log('sys_storage_s3', $e->getMessage()); + return false; + } + + return true; + } + + protected function deleteFileFromEngine($sFilePath, $isPrivate) + { + $sFileLocation = $this->getObjectBaseDir($isPrivate) . $sFilePath; + + try { + if (!$this->_s3->deleteObject($this->_sBucket, $sFileLocation)) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK); + return false; + } + } catch (Exception $e) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK); + bx_log('sys_storage_s3', $e->getMessage()); + 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) + { + $sPath = $this->_bSSL ? 'https://' : 'http://'; + if ($this->_sDomain) + $sPath .= $this->_sDomain; + elseif ($this->_sEndpoint) + $sPath .= $this->_sEndpoint . '/' . $this->_sBucket; + else + $sPath .= $this->_sBucket . '.s3.amazonaws.com'; + return $sPath . '/' . $this->getObjectBaseDir($isPrivate); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4.php new file mode 100644 index 0000000000..513789c142 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4.php @@ -0,0 +1,33 @@ +_s3 = new S3v4\S3( + getParam('sys_storage_s3_access_key'), + getParam('sys_storage_s3_secret_key'), + $this->_bSSL, + $this->_sEndpoint ? $this->_sEndpoint : 's3.amazonaws.com', + getParam('sys_storage_s3_region') + ); + if ($this->_bSSL && getParam('sys_curl_ssl_allow_untrusted')) + $this->_s3->setSSL($this->_bSSL, false); + $this->_s3->setExceptions(true); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4alt.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4alt.php new file mode 100644 index 0000000000..f90ff2607f --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolStorageS3v4alt.php @@ -0,0 +1,197 @@ +setToken($aCredentials['Token']); + if ($this->_sEndpoint) + $oConfiguration->setEndpoint($this->_sEndpoint); + + $this->_s3 = new Akeeba\Engine\Postproc\Connector\S3v4\Connector($oConfiguration); + } + + /** + * Get file url. + * @param $iFileId file + * @return file url + */ + public function getFileUrlById($iFileId) + { + $aFile = $this->_oDb->getFileById($iFileId); + if (!$aFile) + return false; + + if ($this->isAuthUrl($aFile)) { + $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path']; + $sRet = $this->_s3->getAuthenticatedURL($this->_sBucket, $sFileLocation, $this->_aObject['token_life'], $this->_bSSL); + } + else { + $sRet = $this->getObjectBaseUrl($aFile['private']) . $aFile['path']; + } + + if ('s3.wasabisys.com' === $this->_sEndpoint) + $sRet = str_replace('//s3.', '//' . $this->_sBucket . '.s3.', $sRet); + + return $sRet; + } + + public function setFilePrivate($iFileId, $isPrivate = true) + { + // since in S3v4 lib this feature not implemented then we need to get file and upload it back with new ACL + + $aFile = $this->_oDb->getFileById($iFileId); + if (!$aFile) + return false; + + if (!getParam('sys_storage_s3_acl_enable')) + return parent::setFilePrivate($iFileId, 0); + + $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path']; + + $sTmpFile = tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']); + if (!$sTmpFile) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_FILESYSTEM_PERM); + return false; + } + + try { + $this->_s3->getObject($this->_sBucket, $sFileLocation, $sTmpFile); + } catch (Exception $e) { + @unlink($sTmpFile); + $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK); + bx_log('sys_storage_s3v4alt', $e->getMessage()); + return false; + } + + if (getParam('sys_storage_s3_acl_enable')) + $sACL = $isPrivate ? Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_AUTHENTICATED_READ : Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_PUBLIC_READ; + else + $sACL = ''; + $aRequestHeaders = $this->generateHeaders('', $isPrivate, $aFile['mime_type']); + if (!$this->_upload($sTmpFile, $sFileLocation, $sACL, $aRequestHeaders)) + return false; + + return BxDolStorage::setFilePrivate($iFileId, $isPrivate); + } + + // ---------------- + + protected function addFileToEngine($sTmpFile, $sLocalId, $sName, $isPrivate, $iProfileId) + { + $sExt = $this->getFileExt($sName); + $sPath = $this->genPath($sLocalId, $this->_aObject['levels']); + $sRemoteNamePath = $sPath . $sLocalId . ($sExt ? '.' . $sExt : ''); + $aRequestHeaders = $this->generateHeaders($sName, $isPrivate); + if (getParam('sys_storage_s3_acl_enable')) + $sACL = $isPrivate ? Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_AUTHENTICATED_READ : Akeeba\Engine\Postproc\Connector\S3v4\Acl::ACL_PUBLIC_READ; + else + $sACL = ''; + return $this->_upload($sTmpFile, $this->getObjectBaseDir($isPrivate) . $sRemoteNamePath, $sACL, $aRequestHeaders); + } + + protected function _upload($sInputFile, $sUri, $sACL, $aRequestHeaders) + { + try { + $oInputFile = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile); + if (filesize($sInputFile) < BX_DOL_STORAGE_S3V4_MULTIPART_UPLOAD) { + $this->_s3->putObject($oInputFile, $this->_sBucket, $sUri, $sACL, $aRequestHeaders); + } + else { + $sUploadSessionId = $this->_s3->startMultipart($oInputFile, $this->_sBucket, $sUri, $sACL, $aRequestHeaders); + + $sETags = array(); + $sETag = null; + $iPartNumber = 0; + + do + { + // IMPORTANT: You MUST create the input afresh before each uploadMultipart call + $oInput = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile); + $oInput->setUploadID($sUploadSessionId); + $oInput->setPartNumber(++$iPartNumber); + + $sETag = $this->_s3->uploadMultipart($oInput, $this->_sBucket, $sUri); + + if (!is_null($sETag)) + $sETags[] = $sETag; + } + while (!is_null($sETag)); + + // IMPORTANT: You MUST create the input afresh before finalising the multipart upload + $oInput = Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sInputFile); + $oInput->setUploadID($sUploadSessionId); + $oInput->setEtags($sETags); + + $this->_s3->finalizeMultipart($oInput, $this->_sBucket, $sUri); + } + + } catch (Exception $e) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD); + bx_log('sys_storage_s3v4alt', $e->getMessage()); + return false; + } + + return true; + } + + protected function deleteFileFromEngine($sFilePath, $isPrivate) + { + $sFileLocation = $this->getObjectBaseDir($isPrivate) . $sFilePath; + + try { + $this->_s3->deleteObject($this->_sBucket, $sFileLocation); + } catch (Exception $e) { + $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK); + bx_log('sys_storage_s3v4alt', $e->getMessage()); + return false; + } + + return true; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTemplate.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTemplate.php new file mode 100644 index 0000000000..7cf6eec410 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTemplate.php @@ -0,0 +1,3742 @@ + Pages Builder -> Settings. + */ +define('BX_PAGE_TYPE_DEFAULT', 1); ///< default, depends on the settins +define('BX_PAGE_TYPE_DEFAULT_WO_HF', 2); ///< clear page, without any headers and footers +define('BX_PAGE_TYPE_STANDARD', 3); ///< regular page divided on columns +define('BX_PAGE_TYPE_APPLICATION', 4); ///< regular page divided on columns with left vertical menu(s) column + +/** + * Template engine. + * + * An object of the class allows to: + * 1. Manage HTML templates. + * 2. Get URL/path for any template image/icon. + * 3. Attach CSS/JavaScript files to the output. + * 4. Add some content to any template key using Injection engine. + * + * + * Avalable constructions. + * 1. <bx_include_auto:template_name.html /> - the content of the file would be inserted. File would be taken from current template if it existes there, and from base directory otherwise. + * 2. <bx_include_base:template_name.html /> - the content of the file would be inserted. File would be taken from base directory. + * 3. <bx_include_tmpl:template_name.html /> - the content of the file would be inserted. File would be taken from tmpl_xxx directory. + * 4. <bx_url_root /> - the value of BX_DOL_URL_ROOT variable will be inserted. + * 5. <bx_url_admin /> - the value of BX_DOL_URL_ADMIN variable will be inserted. + * 6. <bx_text:_language_key /> - _language_key will be translated using language file(function _t()) and inserted. + <bx_text_js:_language_key /> - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into js string. + <bx_text_attribute:_language_key /> - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into html attribute. + * 7. <bx_image_url:image_file_name /> - image with 'image_file_name' file name will be searched in the images folder of current template. + * If it's not found, then it will be searched in the images folder of base template. On success full URL will be inserted, otherwise an empty string. + * 8. <bx_icon_url:icon_file_name /> - the same with <bx_image_url:image_file_name />, but icons will be searched in the images/icons/ folders. + * 9. <bx_injection:injection_name /> - will be replaced with injections registered with the page and injection_name in the `sys_injections`/`sys_injections_admin`/ tables. + * 10. <bx_if:tag_name>some_HTML</bx_if:tag_name> - will be replaced with provided content if the condition is true, and with empty string otherwise. + * 11. <bx_repeat:cycle_name>some_HTML</bx_repeat:cycle_name> - an inner HTML content will be repeated in accordance with received data. + * + * + * Related classes: + * BxDolTemplateAdmin - for processing admin templates. + * Template classes in modules - for processing modiles' templates. + * + * + * Global variables: + * oSysTemplate - is used for template processing in user part. + * oAdmTemplate - is used for template processing in admin part. + * + * + * Add injection: + * 1. Register it in the `sys_injections` table or `sys_injections_admin` table for admin panel. + * 2. Clear injections cache(sys_injections.inc and sys_injections_admin.inc in cache folder). + * + * + * Predefined template keys to add injections: + * 1. injection_head - add injections in the <head> tag. + * 2. injection_body - add ingection(attribute) in the <body> tag. + * 3. injection_header - add injection inside the <body> tag at the very beginning. + * 4. injection_logo_before - add injection at the left of the main logo(inside logo's DIV). + * 5. injection_logo_after - add injection at the right of the main logo(inside logo's DIV). + * 6. injection_between_logo_top_menu - add injection between logo and top menu. + * 7. injection_top_menu_before - add injection at the left of the top menu(inside top menu's DIV). + * 8. injection_top_menu_after - add injection at the right of the top menu(inside top menu's DIV). + * 13. injection_content_before - add injection just before main content(inside content's DIV). + * 14. injection_content_after - add injection just after main content(inside content's DIV). + * 15. injection_between_content_footer - add injection between content and footer. + * 16. injection_footer_before - add injection at the left of the footer(inside footer's DIV). + * 17. injection_footer_after - add injection at the right of the footer(inside footer's DIV). + * 18. injection_footer - add injection inside the <body> tag at the very end. + * + * + * Example of usage: + * @code + * $oSysTemplate = BxDolTemplate::getInstance(); + * + * $oSysTemplate->addCss(array('test1.css', 'test2.css')); + * $oSysTemplate->addJs(array('test1.js', 'test2.js')); + * $oSysTemplate->parseHtmlByName('messageBox.html', array( + * 'id' => $iId, + * 'msgText' => $sText, + * 'bx_if:timer' => array( + * 'condition' => $iTimer > 0, + * 'content' => array( + * 'id' => $iId, + * 'time' => 1000 * $iTimer, + * 'on_close' => $sOnClose, + * ) + * ), + * 'bx_if:timer' => array( + * array( + * 'name' => $sName, + * 'title' => $sTitle + * ), + * array( + * 'name' => $sName, + * 'title' => $sTitle + * ) + * ) + * )); + * @endcode + * + * + * Memberships/ACL: + * Doesn't depend on user's membership. + * + * + * Alerts: + * no alerts available + * + */ +class BxDolTemplate extends BxDolFactory implements iBxDolSingleton +{ + protected static $_sColorClassPrefix = 'col-'; + protected static $_sColorClassPrefixBg = 'bg-col-'; + protected static $_aColors = array( + 'red1' => array(216, 9, 96), + 'red1-dark' => array(194, 7, 86), + 'red2' => array(231, 68, 30), + 'red2-dark' => array(207, 60, 25), + 'red3' => array(243, 143, 0), + 'red3-dark' => array(218, 128, 0), + 'green1' => array(96, 174, 0), + 'green1-dark' => array(86, 156, 0), + 'green2' => array(209, 211, 0), + 'green2-dark' => array(186, 188, 0), + 'green3' => array(48, 116, 36), + 'green3-dark' => array(43, 104, 32), + 'blue1' => array(10, 61, 143), + 'blue1-dark' => array(9, 54, 128), + 'blue2' => array(0, 164, 165), + 'blue2-dark' => array(0, 146, 148), + 'blue3' => array(0, 160, 206), + 'blue3-dark' => array(0, 143, 184), + 'gray' => array(97, 97, 97), + 'gray-dark' => array(87, 87, 87) + ); + + protected static $_aImages; + protected static $_sImagesCacheKey; + protected static $_iImagesCacheTTL; + + /** + * Main fields + */ + protected $_sName; + protected $_sPrefix; + protected $_sRootPath; + protected $_sRootUrl; + protected $_sSubPath; + protected $_sInjectionsTable; + protected $_sInjectionsCache; + protected $_sCode; + protected $_sCodeKey; + protected $_iMix; + protected $_sMixKey; + protected $_sKeyWrapperHtml; + protected $_sFolderHtml; + protected $_sFolderCss; + protected $_sFolderImages; + protected $_sFolderIcons; + protected $_aTemplates; + + protected $_aLocations; + protected $_aLocationsJs; + + /** + * Cache related fields + */ + protected $_bCacheEnable; + protected $_sCacheFolderUrl; + protected $_sCachePublicFolderUrl; + protected $_sCachePublicFolderPath; + protected $_sCacheFilePrefix; + protected $_aCacheExceptions; + + protected $_bImagesInline; + protected $_iImagesMaxSize; + + protected $_bCssLess; + protected $_bCssCache; + protected $_bCssMinify; + protected $_bCssArchive; + protected $_sCssLessPrefix; + protected $_sCssCachePrefix; + + protected $_bJsLess; + protected $_bJsCache; + protected $_bJsMinify; + protected $_bJsArchive; + protected $_sJsCachePrefix; + + protected $aPage; + protected $aPageContent; + protected $aPageSnapshot = array(); + + protected $_oTemplateConfig; + protected $_oTemplateFunctions; + + /** + * Constructor + */ + protected function __construct($sRootPath = BX_DIRECTORY_PATH_ROOT, $sRootUrl = BX_DOL_URL_ROOT) + { + if(isset($GLOBALS['bxDolClasses'][get_class($this)])) + trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR); + + parent::__construct(); + + $this->_sPrefix = 'BxDolTemplate'; + + $this->_sRootPath = $sRootPath; + $this->_sRootUrl = $sRootUrl; + $this->_sInjectionsTable = 'sys_injections'; + $this->_sInjectionsCache = BX_DOL_TEMPLATE_INJECTIONS_CACHE; + + $this->_sCodeKey = BX_DOL_TEMPLATE_CODE_KEY; + $this->_sMixKey = BX_DOL_TEMPLATE_MIX_KEY; + list( + $this->_sCode, + $this->_sName, + $this->_sSubPath + ) = self::retrieveCode($this->_sCodeKey, $this->_sMixKey, $this->_sRootPath); + + $this->_iMix = 0; + if(is_array($this->_sCode)) + list($this->_sCode, $this->_iMix) = $this->_sCode; + + if(!$this->_sSubPath) + $this->_sSubPath = 'boonex/' . BX_DOL_TEMPLATE_DEFAULT_CODE . '/'; + + if(!file_exists(BX_DIRECTORY_PATH_MODULES . $this->_sSubPath)) // just for 8.0.0-A6 upgrade + $this->_sSubPath = 'boonex/uni/'; + + if(isset($_GET[$this->_sCodeKey])) { + if(BxDolPermalinks::getInstance()->redirectIfNecessary(array($this->_sCodeKey))) + exit; + } + + $this->_sKeyWrapperHtml = '__'; + $this->_sFolderHtml = ''; + $this->_sFolderCss = 'css/'; + $this->_sFolderImages = 'images/'; + $this->_sFolderIcons = 'images/icons/'; + $this->_aTemplates = array('html_tags', 'menu_item_addon', 'menu_item_addon_small', 'menu_item_addon_middle'); + + $this->addLocation('system', $this->_sRootPath, $this->_sRootUrl); + + $this->addLocationJs('system_inc_js', BX_DIRECTORY_PATH_INC . 'js/' , BX_DOL_URL_ROOT . 'inc/js/'); + $this->addLocationJs('system_inc_js_classes', BX_DIRECTORY_PATH_INC . 'js/classes/' , BX_DOL_URL_ROOT . 'inc/js/classes/'); + $this->addLocationJs('system_plugins_public', BX_DIRECTORY_PATH_PLUGINS_PUBLIC, BX_DOL_URL_PLUGINS_PUBLIC); + + $this->_bCacheEnable = !bx_is_dbg() && !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->_aCacheExceptions = ['menu_icon.html']; + + $this->_bImagesInline = getParam('sys_template_cache_image_enable') == 'on'; + $this->_iImagesMaxSize = (int)getParam('sys_template_cache_image_max_size') * 1024; + + $bArchive = getParam('sys_template_cache_compress_enable') == 'on'; + + $this->_bCssLess = true; //--- Less cannot be disabled for CSS. + $this->_bCssCache = !bx_is_dbg() && !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_css_enable') == 'on'; + $this->_bCssMinify = $this->_bCssCache && getParam('sys_template_cache_minify_css_enable') == 'on'; + $this->_bCssArchive = $this->_bCssCache && $bArchive; + $this->_sCssLessPrefix = $this->_sCacheFilePrefix . 'less_'; + $this->_sCssCachePrefix = $this->_sCacheFilePrefix . 'css_'; + + $this->_bJsLess = false; //--- Less language isn't available for JS at all. + $this->_bJsCache = !bx_is_dbg() && !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_js_enable') == 'on'; + $this->_bJsMinify = $this->_bJsCache && getParam('sys_template_cache_minify_js_enable') == 'on'; + $this->_bJsArchive = $this->_bJsCache && $bArchive; + $this->_sJsCachePrefix = $this->_sCacheFilePrefix . 'js_'; + + $this->aPage = array(); + $this->aPageContent = array(); + } + + /** + * Prevent cloning the instance + */ + public function __clone() + { + if (isset($GLOBALS['bxDolClasses'][get_class($this)])) + trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); + } + + /** + * Get singleton instance of the class + */ + public static function getInstance() + { + if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) { + $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolTemplate(); + $GLOBALS['bxDolClasses'][__CLASS__]->init(); + } + + return $GLOBALS['bxDolClasses'][__CLASS__]; + } + + /** + * Retrieve template code and check whether it's associated with active template or not. + * + * @param string $sCodeKey template's code key. + * @param string $sMixKey template's mix key. + * @param string $sRootPath path to root directory. + */ + public static function retrieveCode($sCodeKey = BX_DOL_TEMPLATE_CODE_KEY, $sMixKey = BX_DOL_TEMPLATE_MIX_KEY, $sRootPath = BX_DIRECTORY_PATH_ROOT) + { + $oDb = BxDolDb::getInstance(); + + $fCheckCode = function($sCode, $bSetCookie) use($sCodeKey, $sRootPath) { + if(empty($sCode) || !preg_match('/^[A-Za-z0-9_-]+$/', $sCode)) + return false; + + $aModule = BxDolModuleQuery::getInstance()->getModuleByUri($sCode); + if(empty($aModule) || !is_array($aModule) || (int)$aModule['enabled'] != 1 || !file_exists(BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'data/template/')) + return false; + + $oConfig = new BxDolModuleConfig($aModule); + + $aResult = array( + $oConfig->getUri(), //--- Template module's URI is used as template Code. + $oConfig->getName(), + $oConfig->getDirectory() + ); + + if(!$bSetCookie || bx_get('preview')) + return $aResult; + + bx_setcookie($sCodeKey, $sCode, time() + 60*60*24*365); + + return $aResult; + }; + + $fCheckMix = function($aResult, $iMix, $bSetCookie) use($sMixKey, $sRootPath, $oDb) { + list($sCode, $sName) = $aResult; + if(empty($sName) || empty($iMix)) + return false; + + $aMix = $oDb->getParamsMix($iMix); + if(empty($aMix) || !is_array($aMix) || $aMix['type'] != $sName) + return false; + + if(!$bSetCookie) + return $iMix; + + bx_setcookie($sMixKey, $iMix, time() + 60*60*24*365); + + return $iMix; + }; + + $sCode = getParam('template'); + if(empty($sCode)) + $sCode = BX_DOL_TEMPLATE_DEFAULT_CODE; + $aResult = $fCheckCode($sCode, false); + + //--- Check selected template in COOKIE(the lowest priority) ---// + $sCode = !empty($_COOKIE[$sCodeKey]) ? $_COOKIE[$sCodeKey] : ''; + $aResultCheck = $fCheckCode($sCode, false); + if($aResultCheck !== false) + $aResult = $aResultCheck; + + //--- Check selected template in GET(the highest priority) ---// + $sCode = !empty($_GET[$sCodeKey]) ? $_GET[$sCodeKey] : ''; + $aResultCheck = $fCheckCode($sCode, true); + if($aResultCheck !== false) + $aResult = $aResultCheck; + + if($aResult === false) + return $aResult; + + if(!is_array($aResult[0])) + $aResult[0] = array($aResult[0]); + + $iMixDefault = !empty($aResult[1]) ? (int)getParam($aResult[1] . '_default_mix') : 0; + + //--- Check selected mix in COOKIE(the lowest priority) ---// + $iMix = !empty($_COOKIE[$sMixKey]) ? (int)$_COOKIE[$sMixKey] : 0; + $iResultCheck = $fCheckMix($aResult, $iMix, false); + if($iResultCheck !== false) { + $aMix = $oDb->getParamsMix($iMix); + if((int)$aMix['published'] == 0 && $iMix != $iMixDefault) { + $aUrl = parse_url(BX_DOL_URL_ROOT); + $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/'; + + setcookie($sMixKey, '', time() - 96 * 3600, $sPath); + unset($_COOKIE[$sMixKey]); + } + else + $aResult[0][1] = $iResultCheck; + } + + //--- Check selected mix in GET(the highest priority) ---// + $iMix = !empty($_GET[$sMixKey]) ? (int)$_GET[$sMixKey] : 0; + $iResultCheck = $fCheckMix($aResult, $iMix, true); + if($iResultCheck !== false) + $aResult[0][1] = $iResultCheck; + + //--- Get default mix for currently selected template ---// + if(empty($aResult[0][1]) && !empty($iMixDefault)) { + $iResultCheck = $fCheckMix($aResult, $iMixDefault, false); + if($iResultCheck !== false) + $aResult[0][1] = $iResultCheck; + } + + if(is_array($aResult[0]) && count($aResult[0]) == 1) + $aResult[0] = $aResult[0][0]; + + return $aResult; + } + + public function getIncludedUrls($sType) + { + if (!isset($this->aPage[$sType])) + return array(); + $a = array(); + foreach ($this->aPage[$sType] as $r) + $a[] = $r['url']; + return $a; + } + + /** + * Remember current state of aPage variable with all css, js, etc + */ + public function collectingStart() + { + $this->aPageSnapshot = $this->aPage; + } + + public function collectingInject($aCss, $aJs) + { + $a = array('css' => 'aCss', 'js' => 'aJs'); + foreach ($a as $s => $sVar) { + if (empty($$sVar)) + continue; + $sKey = $s . '_compiled'; + foreach ($$sVar as $r) + $this->aPage[$sKey][] = $r; + } + } + + /** + * Get difference for non-system css and js files from previously remembered state as ready HTML code, + * additionally filter out css and js from $aExcludeCss and $aExcludeJs arrays + */ + public function collectingEndGetCode($aExcludeCss = array(), $aExcludeJs = array(), $sFormat = 'html') + { + if (!is_array($aExcludeCss)) + $aExcludeCss = []; + if (!is_array($aExcludeJs)) + $aExcludeJs = []; + + $aPageSave = $this->aPage; // save current state to restore later + + // filter funcs + $fFilterCss = function ($a) use ($aExcludeCss) { + if (in_array($a['url'], $aExcludeCss)) + return false; + if (isset($this->aPageSnapshot['css_compiled'])) + foreach ($this->aPageSnapshot['css_compiled'] as $r) + if ($r['url'] == $a['url']) + return false; + return true; + }; + $fFilterJs = function ($a) use ($aExcludeJs) { + if (in_array($a['url'], $aExcludeJs)) + return false; + if (isset($this->aPageSnapshot['js_compiled'])) + foreach ($this->aPageSnapshot['js_compiled'] as $r) + if ($r['url'] == $a['url']) + return false; + return true; + }; + + // diff aPageSnapshot and aPage and output only newly added css/js + $this->aPage['css_compiled'] = array_filter($this->aPage['css_compiled'], $fFilterCss); + $this->aPage['js_compiled'] = array_filter($this->aPage['js_compiled'], $fFilterJs); + + // return js/css + $mixedRet = ''; + if ('html' == $sFormat) { + $mixedRet .= $this->includeFiles('css'); + $mixedRet .= $this->includeFiles('js'); + } + else { + $mixedRet = array( + 'css' => $this->aPage['css_compiled'], + 'js' => $this->aPage['js_compiled'], + ); + } + + // restore original state + $this->aPageSnapshot = array(); + $this->aPage = $aPageSave; + + return $mixedRet; + } + + public function getClassName() + { + return get_class($this); + } + + public static function getColorPalette() + { + $aResult = self::$_aColors; + + /** + * @hooks + * @hookdef hook-system-get_color_palette 'system', 'get_color_palette' - hook on get color palette + * - $unit_name - equals `system` + * - $action - equals `get_color_palette` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [array] by ref, array of colors, can be overridden in hook processing + * @hook @ref hook-system-get_color_palette + */ + bx_alert('system', 'get_color_palette', 0, false, array( + 'override_result' => &$aResult + )); + + if($aResult != self::$_aColors) { + $oTemplate = self::getInstance(); + foreach($aResult as $sName => $aRgb) { + $sRgb = 'rgb(' . trim(implode(', ', $aRgb)) . ') !important'; + + $oTemplate->addCssStyle('.' . self::$_sColorClassPrefix . $sName, array( + 'color' => $sRgb + )); + $oTemplate->addCssStyle('.' . self::$_sColorClassPrefixBg . $sName, array( + 'background-color' => $sRgb + )); + } + } + + return $aResult; + } + + public static function getColorCode($mixedName = false, $fOpacity = false) + { + $aPalette = self::getColorPalette(); + $aClasses = array_keys($aPalette); + + if($mixedName === false || (is_string($mixedName) && !is_numeric($mixedName) && !in_array($mixedName, $aClasses))) + $mixedName = $aClasses[rand(0, count($aClasses) - 1)]; + else if(is_numeric($mixedName)) + $mixedName = $aClasses[(int)$mixedName % count($aClasses)]; + + $aColor = $aPalette[$mixedName]; + if($fOpacity !== false && is_numeric($fOpacity)) + $aColor[] = $fOpacity; + + return $aColor; + } + + public static function getColorClass($sType = BX_DOL_COLOR_FT, $sName = '') + { + $aClasses = array_keys(self::getColorPalette()); + + if(empty($sName) || !in_array($sName, $aClasses)) + $sName = $aClasses[rand(0, count($aClasses) - 1)]; + + $sPrefix = ''; + switch ($sType) { + case BX_DOL_COLOR_FT: + $sPrefix = self::$_sColorClassPrefix; + break; + + case BX_DOL_COLOR_BG: + $sPrefix = self::$_sColorClassPrefixBg; + break; + } + + return $sPrefix . $sName; + } + + /** + * Load templates. + */ + function loadTemplates() + { + $aResult = array(); + foreach($this->_aTemplates as $sName) + $aResult[$sName] = $this->getHtml($sName . '.html'); + $this->_aTemplates = $aResult; + } + /** + * Initialize template engine. + * Note. The method is executed with the system, you shouldn't execute it in your subclasses. + */ + function init() + { + $this->loadTemplates(); + + //--- Load page elements related static variables ---// + $this->aPage = array( + 'name_index' => BX_PAGE_DEFAULT, + 'type' => BX_PAGE_TYPE_DEFAULT, + 'url' => '', + 'header' => '', + 'header_text' => '', + 'keywords' => array(), + 'location' => array(), + 'title' => '', + 'description' => '', + 'robots' => '', + 'base' => ['href' => BX_DOL_URL_ROOT], + 'css_name' => array(), + 'css_compiled' => array(), + 'css_system' => array(), + 'css_async' => array(), + 'js_name' => array(), + 'js_compiled' => array(), + 'js_system' => array(), + 'js_options' => array(), + 'js_translations' => array(), + 'js_images' => array(), + 'injections' => array() + ); + + //--- Load default CSS, JS, etc ---// + BxDolPreloader::getInstance()->perform($this); + + //--- Load injection's cache ---// + if (getParam('sys_db_cache_enable')) { + $oDb = BxDolDb::getInstance(); + $oCache = $oDb->getDbCacheObject(); + $sCacheKey = $oDb->genDbCacheKey($this->_sInjectionsCache); + + $aInjections = $oCache->getData($sCacheKey); + if ($aInjections === null) { + $aInjections = $this->getInjectionsData(); + $oCache->setData ($sCacheKey, $aInjections); + } + } + else + $aInjections = $this->getInjectionsData(); + + $this->aPage['injections'] = $aInjections; + + //--- Load images/icons cache ---// + $this->initImages(); + + bx_import('BxTemplConfig'); // TODO: for some reason autoloader isn't working here.... + $this->_oTemplateConfig = BxTemplConfig::getInstance(); + + bx_import('BxTemplFunctions'); + $this->_oTemplateFunctions = BxTemplFunctions::getInstance($this); + + $this->addJsOption('sys_fixed_header'); + $this->addJsOption('sys_confirmation_before_redirect'); + } + + protected function initImages() + { + self::$_iImagesCacheTTL = 86400; + self::$_sImagesCacheKey = 'sys_layout_images_' . $this->_sCode . '_' . bx_site_hash('images') . '.php'; + self::$_aImages = BxDolDb::getInstance()->getDbCacheObject()->getData(self::$_sImagesCacheKey); + if(!self::$_aImages) + self::$_aImages = []; + + /** + * @hooks + * @hookdef hook-system-get_layout_images 'system', 'get_layout_images' - hook on get layout images + * - $unit_name - equals `system` + * - $action - equals `get_layout_images` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `code` - [string] page code + * - `override_result` - [array] by ref, array of images, can be overridden in hook processing + * @hook @ref hook-system-get_layout_images + */ + bx_alert('system', 'get_layout_images', 0, false, [ + 'code' => $this->_sCode, + 'override_result' => &self::$_aImages + ]); + } + + protected function saveImages() + { + if(!self::$_iImagesCacheTTL) + $this->initImages(); + + BxDolDb::getInstance()->getDbCacheObject()->setData(self::$_sImagesCacheKey, self::$_aImages, self::$_iImagesCacheTTL); + } + + 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; + } + + /** + * Set page name index by target. + * @param string $s target. + */ + function setPageNameIndexByTarget($s) + { + $i = BX_PAGE_DEFAULT; + + switch($s) { + case 'bx-content-preload': + $i = BX_PAGE_CONTENT_PRELOAD; + break; + + case 'bx-content-with-toolbar-wrapper': + //$i = 52; + break; + + case 'bx-content-with-cover-wrapper': + $i = BX_PAGE_CONTENT_WITH_COVER; + break; + + case 'bx-content-with-submenu-wrapper': + $i = BX_PAGE_CONTENT_WITH_SUBMENU; + break; + + case 'bx-content-wrapper': + $i = BX_PAGE_CONTENT; + break; + } + + $this->aPage['name_index'] = $i; + } + + /** + * Get page name index + * @return int $i name index + */ + function getPageNameIndex() + { + return isset($this->aPage['name_index']) ? (int)$this->aPage['name_index'] : 0; + } + + /** + * Set page type + * @param int $i page type + */ + function setPageType($i) + { + $this->aPage['type'] = $i; + } + + /** + * Set page url + * @param string $s page url + */ + function setPageUrl($s) + { + $this->aPage['url'] = $s; + } + + /** + * Get page type + * @return int $i page type + */ + function getPageType() + { + $iType = BX_PAGE_TYPE_DEFAULT; + if(isset($this->aPage['type'])) + $iType = (int)$this->aPage['type']; + + if($iType == BX_PAGE_TYPE_DEFAULT) + $iType = (int)getParam('sys_pt_default_' . (isLogged() ? 'member' : 'visitor')); + + return $iType; + } + + /** + * 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 meta title. + * + * @param string sTitle necessary page description. + */ + function setPageMetaTitle($sTitle) + { + $this->aPage['title'] = $sTitle; + } + + /** + * Set page description. + * + * @param string $sDescription necessary page description. + */ + function setPageDescription($sDescription) + { + $this->aPage['description'] = $sDescription; + } + + /** + * Set page meta robots. + * + * @param string $s page meta robots. + */ + function setPageMetaRobots($s) + { + $this->aPage['robots'] = $s; + } + + /** + * Set page injections. + * + * @param array $aInjections name => value injections. + */ + function setPageInjections($aInjections) + { + if(empty($aInjections) || !is_array($aInjections)) + return; + + foreach($aInjections as $sName => $sValue) + $this->addInjection('injection_' . $sName, 'text', $sValue); + } + + /** + * Set page content for some variable. + * @param string $sVar name of content variable + * @param string $sContent content for $sVar variable + * @param int $iIndex optional page index, default is index which was set before with @see setPageNameIndex function, or 0 + */ + function setPageContent($sVar, $sContent, $iIndex = false) + { + $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex(); + $this->aPageContent[$i][$sVar] = $sContent; + } + + /** + * Get page content for some variable. + * @param string $sVar name of content variable + * @param int $iIndex optional page index, default is index which was set before with @see setPageNameIndex function, or 0 + * @return string page content for some variable or for the whole page. + */ + function getPageContent($sVar = false, $iIndex = false) + { + $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex(); + return false !== $sVar ? $this->aPageContent[$i][$sVar] : $this->aPageContent[$i]; + } + + /** + * Get currently active template name. + * + * @return string template's name. + */ + function getName() + { + return $this->_sName; + } + + /** + * Get currently active template name. + * + * @return string template's name. + */ + function getCssClassName() + { + return str_replace('_', '-', $this->_sName); + } + + /** + * Get currently active template code. + * + * @return string template's code. + */ + function getCode() + { + return $this->_sCode; + } + + /** + * Get embed code. + * + * @return string embed's code. + */ + function getEmbed($sContent) + { + if ($sContent == ''){ + header('Content-Security-Policy: frame-ancestors ' . getParam('sys_csp_frame_ancestors')) ; + $this->displayPageNotFound('', BX_PAGE_EMBED); + exit; + } + + $this->addJs(['inc/js/|embed.js']); + $this->addCss(['embed.css']); + $this->aPage['base']['target'] = '_blank'; + $this->setPageNameIndex (BX_PAGE_EMBED); + $this->setPageContent('page_main_code', '
' . $sContent . '
'); + $this->getPageCode(); + } + + /** + * Get code key. + * + * @return string template's code key. + */ + function getCodeKey() + { + return $this->_sCodeKey; + } + + /** + * Get currently active template mix. + * + * @return integer template's mix. + */ + function getMix() + { + return $this->_iMix; + } + + /** + * Get currently active template path. + * + * @return string template's path. + */ + function getPath() + { + return $this->_sSubPath; + } + + /** + * Set page title. + * @deprecated use setPageHeader + * + * @param string $sTitle necessary page title. + */ + function setPageTitle($sTitle) + { + $this->setPageHeader($sTitle); + } + + /** + * Set page's main box title. + * @deprecated use setPageParams + * + * @param string $sTitle necessary page's main box title. + */ + function setPageMainBoxTitle($sTitle) + { + $this->setPageParams(array('header_text' => $sTitle)); + } + + /** + * Check whether location exists or not. + * @param string $sKey - location's unique key. + */ + function isLocation($sKey) + { + return isset($this->_aLocations[$sKey]); + } + + /** + * Get a lis of all added locations. + * @return array with locations. + */ + function getLocations() + { + return $this->_aLocations; + } + + /** + * Add location in array of locations. + * Note. Location is the path/url to folder where 'templates' folder is stored. + * + * @param string $sKey - location's unique key. + * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/' + * @param string $sLocationUrl - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/' + */ + function addLocation($sKey, $sLocationPath, $sLocationUrl) + { + $this->_aLocations[$sKey] = array( + 'path' => $sLocationPath, + 'url' => $sLocationUrl, + ); + + return $sKey; + } + /** + * Add dynamic location. + * + * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/' + * @param string $sLocationUrl - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/' + * @return location key. Is needed to remove the location. + */ + function addDynamicLocation($sLocationPath, $sLocationUrl) + { + $sLocationKey = time() . mt_rand(); + $this->addLocation($sLocationKey, $sLocationPath, $sLocationUrl); + + return $sLocationKey; + } + /** + * Remove location from array of locations. + * Note. Location is the path/url to folder where templates are stored. + * + * @param string $sKey - location's unique key. + */ + function removeLocation($sKey) + { + if(isset($this->_aLocations[$sKey])) + unset($this->_aLocations[$sKey]); + } + /** + * Check whether JS location exists or not. + * @param string $sKey - JS location's unique key. + */ + function isLocationJs($sKey) + { + return isset($this->_aLocationsJs[$sKey]); + } + /** + * Add JS location in array of JS locations. + * Note. Location is the path/url to folder where JS files are stored. + * + * @param string $sKey - location's unique key. + * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/js/' + * @param string $sLocationUrl - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/js/' + */ + function addLocationJs($sKey, $sLocationPath, $sLocationUrl) + { + $this->_aLocationsJs[$sKey] = array( + 'path' => $sLocationPath, + 'url' => $sLocationUrl + ); + + return $sKey; + } + /** + * Add dynamic JS location. + * + * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/' + * @param string $sLocationUrl - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/' + * @return location key. Is needed to remove the location. + */ + function addDynamicLocationJs($sLocationPath, $sLocationUrl) + { + $sLocationKey = time() . mt_rand(); + $this->addLocationJs($sLocationKey, $sLocationPath, $sLocationUrl); + + return $sLocationKey; + } + /** + * Remove JS location from array of locations. + * Note. Location is the path/url to folder where templates are stored. + * + * @param string $sKey - JS location's unique key. + */ + function removeLocationJs($sKey) + { + if(isset($this->_aLocationsJs[$sKey])) + unset($this->_aLocationsJs[$sKey]); + } + /** + * Add Option in JS output. + * + * @param mixed $mixedName option's name or an array of options' names. + */ + function addJsOption($mixedName) + { + if(is_string($mixedName)) + $mixedName = array($mixedName); + + foreach($mixedName as $sName) + $this->aPage['js_options'][$sName] = getParam($sName); + } + /** + * Add language translation for key in JS output. + * + * @param mixed $mixedKey language key or an array of keys. + */ + function addJsTranslation($mixedKey, $bDynamic = false) + { + if(is_string($mixedKey)) + $mixedKey = array($mixedKey); + + foreach($mixedKey as $sKey) + $this->aPage['js_translations'][$sKey] = _t($sKey, '{0}', '{1}'); + + return $bDynamic ? $this->_processJsTranslations() : ''; + } + + /** + * get added js translations + */ + function getJsTranslation($bDynamic = false) + { + return $bDynamic ? $this->_processJsTranslations() : $this->aPage['js_translations']; + } + + /** + * Add image in JS output. + * + * @param array $aImages an array of image descriptors. + * The descriptor is a key/value pear in the array of descriptors. + */ + function addJsImage($aImages) + { + if(!is_array($aImages)) + return; + + foreach($aImages as $sKey => $sFile) { + $sUrl = $this->getImageUrl($sFile); + if(empty($sUrl)) + continue; + + $this->aPage['js_images'][$sKey] = $sUrl; + } + } + /** + * Add icon in JS output. + * + * @param array $aIcons an array of icons descriptors. + * The descriptor is a key/value pear in the array of descriptors. + */ + function addJsIcon($aIcons) + { + if(!is_array($aIcons)) + return; + + foreach($aIcons as $sKey => $sFile) { + $sUrl = $this->getIconUrl($sFile); + if(empty($sUrl)) + continue; + + $this->aPage['js_images'][$sKey] = $sUrl; + } + } + /** + * Add CSS style. + * + * @param string $sName CSS class name. + * @param string $sContent CSS class styles. + */ + function addCssStyle($sName, $sContent) + { + $this->aPage['css_styles'][$sName] = $sContent; + } + /** + * Set page keywords. + * + * @param mixed $mixedKeywords necessary page keywords(string - single keyword, array - an array of keywords). + * @param string $sDevider - string devider. + */ + function addPageKeywords($mixedKeywords, $sDevider = ',') + { + if(is_string($mixedKeywords)) + $mixedKeywords = strpos($mixedKeywords, $sDevider) !== false ? explode($sDevider, $mixedKeywords) : array($mixedKeywords); + + foreach($mixedKeywords as $iKey => $sValue) + $mixedKeywords[$iKey] = trim($sValue); + + $this->aPage['keywords'] = isset($this->aPage['keywords']) && is_array($this->aPage['keywords']) ? array_merge($this->aPage['keywords'], $mixedKeywords) : $mixedKeywords; + } + /** + * Set page locatoin coordinates. + * + * @param $fLat latitude + * @param $fLng longitude + */ + function addPageMetaLocation($fLat, $fLng, $sCountryCode) + { + $this->aPage['location'] = array('lat' => $fLat, 'lng' => $fLng, 'country' => $sCountryCode); + } + /** + * Set page meta image. + * + * @param $sImageUrl meta image url + */ + function addPageMetaImage($sImageUrl) + { + $this->aPage['image'] = $sImageUrl; + } + /** + * Set page rss link. + * + * @param $sTitle - rss feed title + * @param $sUrl - rss feed URL + */ + function addPageRssLink($sTitle, $sUrl) + { + if (!isset($this->aPage['rss'])) + $this->aPage['rss'] = array('title' => $sTitle, 'url' => $sUrl); + else + $this->aPage['rss'] = false; + } + /** + * Returns page meta info, like meta keyword, meta description, location, etc + */ + function getMetaInfo() + { + $sRet = ''; + + $oPage = BxDolPage::getObjectInstanceByURI(); + $bPage = $oPage !== false; + + // general meta tags + if (!empty($this->aPage['keywords']) && is_array($this->aPage['keywords'])) + $sRet .= ''; + + $sDescription = ''; + if(!empty($this->aPage['description']) && is_string($this->aPage['description'])) + $sDescription = $this->aPage['description']; + if(!$sDescription && $bPage) + $sDescription = $oPage->getMetaDescription(); + $bDescription = !empty($sDescription); + + if ($bDescription) + $sRet .= ''; + + // location + if (!empty($this->aPage['location']) && isset($this->aPage['location']['lat']) && isset($this->aPage['location']['lng']) && isset($this->aPage['location']['country'])) + $sRet .= ' + + + '; + + //set meta[image] value + if(empty($this->aPage['image'])) { + // use cover image if exists + if($bPage && ($aCover = $oPage->getPageCoverImage())) + $this->aPage['image'] = BxDolCover::getInstance($this)->getCoverImageUrl($aCover); + + // use system Apple/Android icons if exists + if(empty($this->aPage['image'])) { + $oImgStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES); + foreach(['icon_android_splash', 'icon_android', 'icon_apple'] as $sIcon) + if(($iIcon = (int)getParam('sys_site_' . $sIcon)) != 0 && ($sUrl = $oImgStorage->getFileUrlById($iIcon))) { + $this->aPage['image'] = $sUrl; + break; + } + } + } + + // facebook / twitter + $bPageImage = !empty($this->aPage['image']); + $sRet .= ''; + if ($bPageImage) + $sRet .= ''; + $sRet .= ''; + $sRet .= ''; + + // Smart App Banner + if (getParam('smart_app_banner') && false === strpos($_SERVER['HTTP_USER_AGENT'], 'UNAMobileApp')) { + if ($sAppIdIOS = getParam('smart_app_banner_ios_app_id')) + $sRet .= ''; + } + + // RSS + $oFunctions = BxTemplFunctions::getInstance(); + $sRet .= $oFunctions->getManifests(); + $sRet .= $oFunctions->getMetaIcons(); + + if (!empty($this->aPage['rss']) && !empty($this->aPage['rss']['url'])) + $sRet .= ''; + + $sRet .= "aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . "\" />"; + + if (!empty($this->aPage['url'])){ + $sRet .= ''; + } + + return $sRet; + } + /** + * Get template, which was loaded earlier. + * @see method this->loadTemplates and field this->_aTemplates + * + * @param string $sName - template name. + * @return string template's content. + */ + public function getTemplate($sName) + { + return $this->_aTemplates[$sName]; + } + + /** + * Get template functions object. + * @see BxBaseFunctions + */ + public function getTemplateFunctions() + { + return $this->_oTemplateFunctions; + } + + /** + * Get image MIME type. + * + * @param string $sExtension - image file's extension. + * @return string with MIME type. + */ + function getImageMimeType($sExtension) + { + $sExtension = strtolower($sExtension); + + $sResult = ''; + switch($sExtension) { + case 'svg': + $sResult = 'svg+xml'; + break; + + default: + $sResult = $sExtension; + } + + return 'data:image/' . $sResult; + } + + /** + * Get icon template in dependence of a value, provided in $mixedId. + * + * @param mixed $mixedId numeric id from Storage, string with template's file name or string with font icon. + */ + public function getIcon($mixedId, $aParams = array()) + { + return $this->_getImage('icon', $mixedId, $aParams); + } + + /** + * Get image template in dependence of a value, provided in $mixedId. + * + * @param mixed $mixedId numeric id from Storage, string with template's file name or string with font icon. + */ + public function getImage($mixedId, $aParams = array()) + { + return $this->_getImage('image', $mixedId, $aParams); + } + + protected function _getImage($sType, $mixedId, $aParams = array()) + { + $sUrl = ""; + $aType2Method = array('image' => 'getImageUrl', 'icon' => 'getIconUrl'); + + //--- Check in System Storage. + if(is_numeric($mixedId) && (int)$mixedId > 0) { + $sStorage = BX_DOL_STORAGE_OBJ_IMAGES; + if(!empty($aParams['storage'])) { + $sStorage = $aParams['storage']; + unset($aParams['storage']); + } + + if(($sResult = BxDolStorage::getObjectInstance($sStorage)->getFileUrlById((int)$mixedId)) !== false) + $sUrl = $sResult; + } + + //--- Check in template folders. + if($sUrl == "" && is_string($mixedId) && strpos($mixedId, '.') !== false) + $sUrl = $this->{$aType2Method[$sType]}($mixedId); + + if($sUrl != "") + return $this->parseImage($sUrl, array( + 'class' => isset($aParams['class']) && !empty($aParams['class']) ? $aParams['class'] : '', + 'alt' => isset($aParams['alt']) && !empty($aParams['alt']) ? $aParams['alt'] : '' + )); + + //--- Use iconic font. + return $this->parseIcon($mixedId, $aParams); + } + + /** + * Get full URL for the icon. + * + * @param string $sName icon's file name. + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return string full URL. + */ + function getIconUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + $sContent = ""; + if(($sContent = $this->_getInlineData('icon', $sName, $sCheckIn)) !== false) + return $sContent; + + return $this->_getAbsoluteLocation('url', $this->_sFolderIcons, $sName, $sCheckIn); + } + /** + * Get absolute Path for the icon. + * + * @param string $sName - icon's file name. + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return string absolute path. + */ + function getIconPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + return $this->_getAbsoluteLocation('path', $this->_sFolderIcons, $sName, $sCheckIn); + } + + /** + * Get image/icon by name automatically. Cache item description: + * [ + * 'v' => value (name, url or source), + * 'c' => classes list divided with comma (,) + * 't' => parse type (ic - icon, im - image, sc - source) + * ] + * Cached images can be overwritten by listening 'system' - 'get_layout_images' alert. + * + * @param string $sName unique name. The following format can be used: name|classes. + * Where name and classes can consists of multiple parts (divided with comma (,) in HTML variant). For example, + * in PHP: $this->getImageAuto('far star|class1 class2'); + * in HTML: + * @param boolean $bWrapped wrap in HTML tag or not. + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return string icon/image value (name, url or source) or final HTML code. + */ + function getImageAuto($sName, $bWrapped = true, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + $sDivParts = '|'; + $sDivItems = ','; + + $sKey = md5($sName); + + $sClasses = ''; + if(strpos($sName, $sDivParts) !== false) + list($sName, $sClasses) = explode($sDivParts, $sName); + + if(strpos($sName, $sDivItems) !== false) + $sName = implode(' ', explode($sDivItems, $sName)); + + if(!isset(self::$_aImages[$sKey])) { + $aResult = [ + 'v' => $sName, + 't' => 'ic', + 'c' => $sClasses + ]; + + $sUrl = ''; + foreach(['Image', 'Icon'] as $sType) + foreach(['svg', 'png', 'jpg', 'gif'] as $sExt) + if(($sUrl = $this->{'get' . $sType . 'Url'}($sName . '.' . $sExt, $sCheckIn)) != '') { + $aResult = [ + 'v' => $sUrl, + 't' => 'im', + 'c' => $sClasses + ]; + break 2; + } + + self::$_aImages[$sKey] = $aResult; + + $this->saveImages(); + } + + if(!self::$_aImages[$sKey]['v']) + return ''; + + if(!$bWrapped || self::$_aImages[$sKey]['t'] == 'sc') + return self::$_aImages[$sKey]['v']; + + $aAttrs = []; + if(self::$_aImages[$sKey]['c'] != '') + $aAttrs['class'] = implode(' ', explode($sDivItems, self::$_aImages[$sKey]['c'])); + + $aType2Method = ['ic' => 'parseIcon', 'im' => 'parseImage']; + return $this->{$aType2Method[self::$_aImages[$sKey]['t']]}(self::$_aImages[$sKey]['v'], $aAttrs); + } + + /** + * 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) + { + if(($aFile = $this->_locateFile('css', $sName)) !== false) + return $aFile[0]; + + return $this->_getAbsoluteLocation('url', $this->_sFolderCss, $sName, $sCheckIn); + } + function getCssUrlWithRevision($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + $sUrl = $this->getCssUrl($sName, $sCheckIn); + if(!empty($sUrl)) + $sUrl = $this->addRevision($sUrl); + + return $sUrl; + } + + /** + * 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) + { + if(($aFile = $this->_locateFile('css', $sName)) !== false) + return $aFile[1]; + + 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) + { + if(($aFile = $this->_locateFile('js', $sName)) !== false) + return $aFile[0]; + + return $this->_getAbsoluteLocationJs('url', $sName); + } + function getJsUrlWithRevision($sName) + { + $sUrl = $this->getJsUrl($sName); + if(!empty($sUrl)) + $sUrl = $this->addRevision($sUrl); + + return $sUrl; + } + /** + * Get full Path of JS file. + * + * @param string $sName - JS file name. + * @return string full URL. + */ + function getJsPath($sName) + { + if(($aFile = $this->_locateFile('js', $sName)) !== false) + return $aFile[1]; + + 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); + + if($s == 'sys_site_submenu'){ + $oPage = BxDolPage::getObjectInstanceByURI(); + if ($oPage && $oPage->getSubMenu() == 'disabled'){ + return; + } + } + return $oMenu ? $oMenu->getCode () : ''; + } + + /** + * Check whether HTML file exists or not. + * + * @param string $sName - HTML file name. + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return boolean result of operation. + */ + function isHtml($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + return $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn) != ''; + } + + /** + * Get content of HTML file. + * + * @param string $sName - HTML file name. + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return string full content of the file and false on failure. + */ + function getHtml($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn); + return !empty($sAbsolutePath) ? trim(file_get_contents($sAbsolutePath)) : false; + } + + /** + * Parse HTML template. Search for the template with accordance to it's file name. + * + * @see allows to use cache. + * + * @param string $sName - HTML file name. + * @param array $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix. + * @param mixed $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise). + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return string the result of operation. + */ + function parseHtmlByName($sName, $aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH) + { + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginTemplate($sName, $sRand = time().rand()); + + if (($sContent = $this->getCached($sName, $aVariables, $mixedKeyWrapperHtml, $sCheckIn)) !== false) { + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sContent, true); + return $sContent; + } + + $sRet = ''; + if (($sContent = $this->getHtml($sName, $sCheckIn)) !== false) + $sRet = $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml); + + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sRet, false); + + return $sRet; + } + /** + * Parse HTML template. + * + * @see Doesn't allow to use cache. + * + * @param string $sContent - HTML file content. + * @param array $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix. + * @param mixed $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise). + * @return string the result of operation. + */ + function parseHtmlByContent($sContent, $aVariables, $mixedKeyWrapperHtml = null) + { + if(empty($sContent)) + return ""; + + return $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml); + } + /** + * Parse earlier loaded HTML template. + * + * @see Doesn't allow to use cache. + * + * @param string $sName - template name. + * @param array $aVariables - key/value pairs. Key should be the same as template's key, excluding prefix and postfix. + * @return string the result of operation. + * @see $this->_aTemplates + */ + function parseHtmlByTemplateName($sName, $aVariables, $mixedKeyWrapperHtml = null) + { + if(!isset($this->_aTemplates[$sName]) || empty($this->_aTemplates[$sName])) + return ""; + + return $this->_parseContent($this->_aTemplates[$sName], $aVariables, $mixedKeyWrapperHtml); + } + /** + * Parse page HTML template. Search for the page's template with accordance to it's file name. + * + * @see allows to use cache. + * + * @param string $sName - HTML file name. + * @param array $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix. + * @return string the result of operation. + */ + function parsePageByName($sName, $aVariables) + { + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginPage($sName); + + $sContent = $this->parseHtmlByName($sName, $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH); + if(empty($sContent)) { + $aType = BxDolPageQuery::getPageType($this->getPageType()); + if(!empty($aType) && is_array($aType)) + $sContent = $this->parseHtmlByName($aType['template'], $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH); + } + if(empty($sContent)) + $sContent = $this->parseHtmlByName('default.html', $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH); + + //---Process injection at the very last ---// + $oTemplate = &$this; + $sContent = preg_replace_callback("''s", function($aMatches) use($oTemplate) { + return $oTemplate->processInjection($oTemplate->getPageNameIndex(), $aMatches[1]); + }, $sContent); + + //--- Add CSS and JS at the very last ---// + if(strpos($sContent, '') !== false) { + $aStyles = array( + 'display' => 'none !important' + ); + + if(isLogged()) + $this->addCssStyle('.bx-hide-when-logged-in', $aStyles); + else + $this->addCssStyle('.bx-hide-when-logged-out', $aStyles); + + $sContent = str_replace('', $this->includeCssStyles(), $sContent); + } + + if(strpos($sContent , '') !== false) { + $sContent = str_replace('', $this->includeFiles('css', true), $sContent); + } + + if(strpos($sContent , '') !== false) { + if (!empty($this->aPage['css_name'])) + $this->addCss($this->aPage['css_name']); + $sContent = str_replace('', $this->includeFiles('css'), $sContent); + } + + if(strpos($sContent , '') !== false) { + $sContent = str_replace('', $this->includeFiles('js', true), $sContent); + } + + if(strpos($sContent , '') !== false) { + if (!empty($this->aPage['js_name'])) + $this->addJs($this->aPage['js_name']); + $sContent = str_replace('', $this->includeFiles('js') . $this->includeCssAsync(), $sContent); + } + + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endPage($sContent); + + return $sContent; + } + /** + * Parse system keys. + * + * @param string $sKey key + * @return string value associated with the key. + */ + function parseSystemKey($sKey, $mixedKeyWrapperHtml = null, $bProcessInjection = true) + { + $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml); + + $sRet = ''; + switch( $sKey ) { + case 'page_charset': + $sRet = 'UTF-8'; + break; + case 'page_robots': + if(!empty($this->aPage['robots']) && is_string($this->aPage['robots'])) + $sRet = ''; + break; + case 'meta_info': + $sRet = $this->getMetaInfo(); + break; + case 'page_header': + if (isset($this->aPage['title']) && !empty($this->aPage['title'])) + $sRet = bx_process_output(strip_tags($this->aPage['title'])); + if(empty($sRet) && isset($this->aPage['header'])) + $sRet = bx_process_output(strip_tags($this->aPage['header'])); + break; + case 'page_header_text': + if(isset($this->aPage['header_text'])) + $sRet = bx_process_output($this->aPage['header_text']); + break; + case 'page_width': + if (false === strpos($this->_oTemplateConfig->aLessConfig['bx-page-width'], 'px')) + $sRet = BX_DOL_PAGE_WIDTH; + else + $sRet = $this->_oTemplateConfig->aLessConfig['bx-page-width']; + break; + case 'page_viewport': + $sRet = getParam('sys_viewport_meta_tag'); + break; + case 'system_injection_head': + $sRet = $this->_oTemplateFunctions->getInjectionHead(); + break; + case 'system_injection_header': + $sRet = $this->_oTemplateFunctions->getInjectionHeader(); + break; + case 'system_injection_footer': + $sRet = $this->_oTemplateFunctions->getInjectionFooter(); + break; + case 'lang': + $sRet = bx_lang_code(); + break; + case 'lang_direction': + $sRet = bx_lang_direction(); + break; + case 'lang_country': + if (!($sRet = BxDolLanguages::getInstance()->getLangCountryCode())) + $sRet = bx_lang_country(); + 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); + + $bCover = $oCover->isEnabled(); + if($bCover && ($oPage = BxDolPage::getObjectInstanceByURI()) !== 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 'site_submenu_class': + $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu'); + if($oMenu) + $sRet = $oMenu->getClass(); + break; + case 'site_submenu_hidden': + $sClass = 'bx-menu-main-bar-hidden'; + + $oPage = BxDolPage::getObjectInstanceByURI(); + if($oPage !== false && !$oPage->isVisiblePageSubmenu()) { + $sRet = $sClass; + break; + } + + $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu'); + $oMenuManage = BxDolMenu::getObjectInstance('sys_site_manage'); + if($oMenuSubmenu !== false && !$oMenuSubmenu->isVisible() && $oMenuManage !== false && !$oMenuManage->isVisible()) { + $sRet = $sClass; + break; + } + + break; + case 'dol_images': + $sRet = $this->_processJsImages(); + break; + case 'dol_lang': + $sRet = $this->_processJsTranslations(); + break; + case 'dol_options': + $sRet = $this->_processJsOptions(); + break; + case 'copyright': + $sRet = _t( '_copyright', date('Y') ) . getVersionComment(); + break; + case 'copyright_attr': + $sRet = bx_html_attribute(_t('_copyright', date('Y'))); + break; + case 'extra_js': + $sRet = empty($this->aPage['extra_js']) ? '' : $this->aPage['extra_js']; + break; + case 'is_profile_page': + $sRet = (defined('BX_PROFILE_PAGE')) ? 'true' : 'false'; + break; + case 'system_js_requred': + $sRet = _t('_sys_javascript_requred'); + break; + case 'included_css': + $sRet = json_encode($this->getIncludedUrls('css_compiled')); + break; + case 'included_js': + $sRet = json_encode($this->getIncludedUrls('js_compiled')); + break; + case 'base': + $sRet = bx_convert_array2attrs($this->aPage['base']); + break; + case 'class_name': + $sRet = $this->getCssClassName(); + + if (preg_match('/^[A-Za-z0-9_\-]+$/', bx_get('i'))) + $sRet .= ' bx-page-' . bx_get('i'); + + if(!empty($this->_iMix)) { + $aMix = BxDolDb::getInstance()->getParamsMix($this->_iMix); + if(isset($aMix['dark']) && (int)$aMix['dark'] == 1) + $sRet .= ' dark'; + } + break; + case 'css_media_phone': + case 'css_media_phone2': + case 'css_media_tablet': + case 'css_media_tablet2': + case 'css_media_desktop': + $aData = json_decode(getParam('sys_css_media_classes'), true); + $sKey = str_replace('css_media_', '', $sKey); + $sRet = $aData[$sKey]; + break; + case 'service_worker': + if(getParam('sys_pwa_sw_enable') != 'on') + break; + $sRet = "if(navigator && navigator.serviceWorker) navigator.serviceWorker.register('sw.js.php');"; + break; + case 'socket_engine': + $sRet = BxDolSockets::getInstance()->getJsCode(); + break; + + case 'info': + $sRet = 'L:' . bx_get_logged_profile_id(); + if(($oPage = BxDolPage::getObjectInstanceByURI()) !== false && method_exists($oPage, 'getContentInfo')) { + $aContentInfo = $oPage->getContentInfo(); + if(isset($aContentInfo['id'])) + $sRet .= '-C:' . (int)$aContentInfo['id']; + if(isset($aContentInfo['profile_id'])) + $sRet .= '-P:' . (int)$aContentInfo['profile_id']; + } + break; + + default: + $sRet = ($sTemplAdd = BxTemplFunctions::getInstance()->TemplPageAddComponent($sKey)) !== false ? $sTemplAdd : $aKeyWrappers['left'] . $sKey . $aKeyWrappers['right']; + } + + if($bProcessInjection) + $sRet = $this->processInjection($this->getPageNameIndex(), $sKey, $sRet); + + return $sRet; + } + + /** + * Parse tag + * + * @param string $sLink link URL + * @param string $sContent link content + * @param array $aAttrs an array of key => value pairs + */ + function parseLink($sLink, $sContent, $aAttrs = array()) + { + $sAttrs = ''; + foreach($aAttrs as $sKey => $sValue) + $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"'; + + return '' . $sContent . ''; + } + + /** + * Parse tag using provided HTML template + * + * @param string $sName template's file name + * @param string $sLink link URL + * @param string $sContent link content + * @param array $aAttrs an array of key => value pairs + */ + function parseLinkByName($sName, $sLink, $sContent, $aAttrs = array()) + { + $sAttrs = ''; + foreach($aAttrs as $sKey => $sValue) + $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"'; + + return $this->parseHtmlByName($sName, array( + 'href' => $sLink, + 'attrs' => $sAttrs, + 'content' => $sContent + )); + } + + /** + * Parse tag '; + } + + /** + * Parse tag + * + * @param string $sLink URL to image source + * @param array $aAttrs an array of key => value pairs + */ + function parseImage($sLink, $aAttrs = array()) + { + $sAttrs = ''; + foreach($aAttrs as $sKey => $sValue) + $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"'; + + return ''; + } + + /** + * Parse font based icon in tag + * + * @param string $sName font icon name + * @param array $aAttrs an array of key => value pairs + */ + function parseIcon($sName, $aAttrs = array()) + { + $aIcons = BxTemplFunctions::getInstance()->getIcon($sName, $aAttrs); + if($aIcons[0] != '') + $aIcons[0] = ''; + + return implode($aIcons); + } + + function getCacheFilePrefix($sType) + { + $sResult = ''; + switch($sType) { + case 'template': + $sResult = $this->_sCacheFilePrefix; + break; + + case 'less': + $sResult = $this->_sCssLessPrefix; + break; + + case 'css': + $sResult = $this->_sCssCachePrefix; + break; + + case 'js': + $sResult = $this->_sJsCachePrefix; + break; + } + + return $sResult; + } + /** + * Get cache object for templates + * @return cache class instance + */ + function getTemplatesCacheObject () + { + $sCacheEngine = getParam('sys_template_cache_engine'); + $oCacheEngine = bx_instance('BxDolCache' . $sCacheEngine); + if(!$oCacheEngine->isAvailable()) + $oCacheEngine = bx_instance('BxDolCacheFileHtml'); + return $oCacheEngine; + } + /** + * Get template from cache if it's enabled. + * + * @param string $sName template name + * @param string $aVariables key/value pairs. key should be the same as template's key, but without prefix and postfix. + * @param mixed $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise). + * @param string $sCheckIn where the content would be searched(base, template, both) + * @param boolean $bEvaluate need to evaluate the template or not. + * @return string result of operation or false on failure. + */ + function getCached($sName, &$aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH, $bEvaluate = true) + { + // initialization + + if(!$this->_bCacheEnable) + return false; + + if (in_array($sName, $this->_aCacheExceptions)) + 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; + } + + public function clearTemplatesCache($sType = '') + { + if(!$sType) + $sType = 'template'; + + $this->getTemplatesCacheObject()->removeAllByPrefix($this->getCacheFilePrefix($sType)); + } + + public function clearImagesCache($sCode = '') + { + if(!$sCode) + $sCode = $this->_sCode; + + BxDolDb::getInstance()->getDbCacheObject()->removeAllByPrefix('sys_layout_images_' . $sCode . '_'); + } + + /** + * 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); + } + + function addJsPreloaded($aFiles, $sCallback = false, $sCondition = false, $sConditionElseCallback = false) + { + if(!$aFiles) + return ''; + + if(!is_array($aFiles)) + $aFiles = [$aFiles]; + + $sMaskLoad = "bx_get_scripts(%s);"; + $sMaskLoadWithCallback = "bx_get_scripts(%s, function() {%s});"; + + $sMaskCondition = "if(%s) {%s}"; + $sMaskConditionWithElse = "if(%s) {%s} else {setTimeout(function() {%s}, 10);}"; + + $aFilesLocated = []; + foreach($aFiles as $sFile) { + $mixedFile = $this->_locateFile('js', $sFile); + if($mixedFile === false) + continue; + + list($sUrl) = $mixedFile; + + $aFilesLocated[] = $this->addRevision($sUrl); + } + $sFilesLocated = json_encode($aFilesLocated); + + if($sCallback !== false) + $sResult = sprintf($sMaskLoadWithCallback, $sFilesLocated, $sCallback); + else + $sResult = sprintf($sMaskLoad, $sFilesLocated); + + if($sCondition === false) + return $sResult; + + if($sConditionElseCallback !== false) + $sResult = sprintf($sMaskConditionWithElse, $sCondition, $sResult, $sConditionElseCallback); + else + $sResult = sprintf($sMaskCondition, $sCondition, $sResult); + + return $sResult; + } + + function addJsPreloadedWrapped($aFiles, $sCallback = false, $sCondition = false, $sConditionElseCallback = false) + { + $sCode = $this->addJsPreloaded($aFiles, $sCallback, $sCondition, $sConditionElseCallback); + if(!$sCode) + return ''; + + return $this->_wrapInTagJsCode($sCode); + } + + function addJsCodeOnLoad($sCallback) + { + $sMaskLoad = "$(document).ready(function() {%s});"; + + return sprintf($sMaskLoad, $sCallback); + } + + function addJsCodeOnLoadWrapped($sCallback) + { + $sCode = $this->addJsCodeOnLoad($sCallback); + + return $this->_wrapInTagJsCode($sCode); + } + + /** + * get added js files + */ + function getJs() + { + return $this->aPage['js_compiled']; + } + + /** + * Add System JS file(s) to global output. + * System JS files are the files which are attached to all pages. They will be cached separately from the others. + * + * @param mixed $mixedFiles string value represents a single JS file name. An array - array of JS file names. + * @param boolean $bDynamic in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly. + * @return boolean/string result of operation. + */ + function addJsSystem($mixedFiles) + { + return $this->_processFiles('js', 'add', $mixedFiles, false, true); + } + + /** + * Delete JS file(s) from global output. + * + * @param mixed $mixedFiles string value represents a single JS file name. An array - array of JS file names. + * @return boolean result of operation. + */ + function deleteJs($mixedFiles) + { + return $this->_processFiles('js', 'delete', $mixedFiles); + } + + /** + * Delete System JS file(s) from global output. + * + * @param mixed $mixedFiles string value represents a single JS file name. An array - array of JS file names. + * @return boolean result of operation. + */ + function deleteJsSystem($mixedFiles) + { + return $this->_processFiles('js', 'delete', $mixedFiles, false, true); + } + + /** + * Compile JS files in one file. + * + * @param string $sAbsolutePath CSS file absolute path(full URL for external CSS/JS files). + * @param array $aIncluded an array of already included JS files. + * @return string result of operation. + */ + function _compileJs($sAbsolutePath, &$aIncluded) + { + if(isset($aIncluded[$sAbsolutePath])) + return ''; + + $bExternal = strpos($sAbsolutePath, "http://") !== false || strpos($sAbsolutePath, "https://") !== false; + if($bExternal) { + $sPath = $sAbsolutePath; + $sName = ''; + + $sContent = bx_file_get_contents($sAbsolutePath); + } else { + $aFileInfo = pathinfo($sAbsolutePath); + $sPath = $aFileInfo['dirname'] . DIRECTORY_SEPARATOR; + $sName = $aFileInfo['basename']; + + $sContent = file_get_contents($sPath . $sName); + } + + if(empty($sContent)) + return ''; + + $sUrl = bx_ltrim_str($sPath, realpath(BX_DIRECTORY_PATH_ROOT), BX_DOL_URL_ROOT); + $sUrl = str_replace(DIRECTORY_SEPARATOR, '/', $sUrl); + + $sContent = "\r\n/*--- BEGIN: " . $sUrl . $sName . "---*/\r\n" . $sContent . ";\r\n/*--- END: " . $sUrl . $sName . "---*/\r\n"; + $sContent = preg_replace("/\/\/# sourceMappingURL\s*=.*/si", "", $sContent); + $sContent = str_replace(["\n\r", "\r\n", "\r"], "\n", $sContent); + + $aIncluded[$sAbsolutePath] = 1; + + return preg_replace( + array( + "''", + "'\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 ""; + } + + /** + * Wrap JS code into JS tag. + * + * @param string $sCode - JS code. + * @return string the result of operation. + */ + function _wrapInTagJsCode($sCode) + { + return ""; + } + + /** + * 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) + { + if($bDynamic) + return $this->addCssPreloadedWrapped($mixedFiles); + else + return $this->_processFiles('css', 'add', $mixedFiles, $bDynamic); + } + + function addCssPreloaded($aFiles) + { + if(!$aFiles) + return ''; + + if(!is_array($aFiles)) + $aFiles = [$aFiles]; + + $sMaskLoad = "bx_get_style(%s);"; + + $aFilesLocated = []; + foreach($aFiles as $sFile) { + $mixedFile = $this->_locateFile('css', $sFile); + if($mixedFile === false) + continue; + + list($sUrl) = $mixedFile; + + $aFilesLocated[] = $this->addRevision($sUrl); + } + $sFilesLocated = json_encode($aFilesLocated); + + return sprintf($sMaskLoad, $sFilesLocated); + } + + function addCssPreloadedWrapped($aFiles) + { + $sCode = $this->addCssPreloaded($aFiles); + if(!$sCode) + return ''; + + return $this->_wrapInTagJsCode($sCode); + } + + /** + * get added css files + */ + function getCss() + { + return $this->aPage['css_compiled']; + } + + /** + * Add additional heavy css file (not very necessary) to load asynchronously for desktop browsers only + * @param mixed $mixedFiles string value represents a single CSS file name. An array - array of CSS file names. + */ + function addCssAsync($mixedFiles) + { + if (!is_array($mixedFiles)) + $mixedFiles = array($mixedFiles); + + foreach ($mixedFiles as $sFile) + $this->aPage['css_async'][] = $this->_getAbsoluteLocationCss('url', $sFile); + + $this->addJs('loadCSS.js'); + } + + /** + * Return script tag with special code to load async css. + * This tag is added after js files list + */ + function includeCssAsync () + { + if (empty($this->aPage['css_async'])) + return ''; + + $this->aPage['css_async'] = array_unique($this->aPage['css_async']); + + $sList = ''; + foreach ($this->aPage['css_async'] as $sUrl) + $sList .= 'loadCSS("' . $sUrl . '", document.getElementById("bx_css_async"));'; + + // don't load css for mobile devices + return ' + + '; + } + + /** + * 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) + { + 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; + + $aFiles = array($mixed['path'] => $mixed['url']); + $aOptions = array('cache_dir' => $this->_sCachePublicFolderPath, 'prefix' => $this->_sCssLessPrefix); + $sFile = Less_Cache::Get($aFiles, $aOptions, $this->_oTemplateConfig->aLessConfig); + + return array('url' => $this->_sCachePublicFolderUrl . $sFile, 'path' => $this->_sCachePublicFolderPath . $sFile); + } + + $oLess = new Less_Parser(); + $oLess->ModifyVars($this->_oTemplateConfig->aLessConfig); + $oLess->parse($mixed); + return $oLess->getCss(); + } + + /** + * Minify CSS + * + * @param string $s CSS string to minify + * @return string minified CSS string. + */ + function _minifyCss($s) + { + return BxDolMinify::getInstance()->minifyCss($s); + } + + /** + * Wrap an URL to CSS file into CSS tag. + * + * @param string $sFile - URL to CSS file. + * @return string the result of operation. + */ + function _wrapInTagCss($sFile) + { + if (!$sFile) + return ''; + return ""; + } + /** + * Wrap CSS code into CSS tag. + * + * @param string $sCode - CSS code. + * @return string the result of operation. + */ + function _wrapInTagCssCode($sCode) + { + return ""; + } + /** + * Include CSS style(s) in the page's head section. + */ + function includeCssStyles() + { + $sResult = ""; + if(empty($this->aPage['css_styles']) || !is_array($this->aPage['css_styles'])) + return $sResult; + + foreach($this->aPage['css_styles'] as $sName => $aContent) { + $sContent = ""; + if(!empty($aContent) && is_array($aContent)) + foreach($aContent as $sStyleName => $sStyleValue) + $sContent .= "\t" . $sStyleName . ": " . $sStyleValue . ";\r\n"; + + $sResult .= $sName . " {\r\n" . $sContent . "}\r\n"; + } + + return !empty($sResult) ? $this->_wrapInTagCssCode($sResult) : ''; + } + /** + * Include CSS/JS file(s) attached to the page in its head section. + * @see the method is system and would be called automatically. + * + * @param string $sType the type of file('js' or 'css') + * @return string the result CSS code. + */ + function includeFiles($sType, $bSystem = false, $bWrap = true) + { + $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, $bWrap); + + //--- 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 $bWrap ? $this->$sMethodWrap($sCacheAbsoluteUrl) : $sCacheAbsoluteUrl; + } + + //--- Collect all attached CSS/JS in one file ---// + + $sResult = ""; + $aIncluded = array(); + foreach($aFiles as $aFile) { + if($this->{'_b' . $sUpcaseType . 'Less'}) + $aFile = $this->$sMethodLess($aFile); + + if(($sContent = $this->$sMethodCompile($aFile['path'], $aIncluded)) === false) + continue; + + if(!preg_match('/[\.-]min.(js|css)$/i', $aFile['path']) && $this->{'_b' . $sUpcaseType . 'Minify'}) // don't minify minified files + $sContent = $this->$sMethodMinify($sContent); + + $sResult .= $sContent; + } + + $mixedWriteResult = false; + if(!empty($sResult) && ($rHandler = fopen($sCacheAbsolutePath, 'w')) !== false) { + $mixedWriteResult = fwrite($rHandler, $sResult); + fclose($rHandler); + @chmod ($sCacheAbsolutePath, BX_DOL_FILE_RIGHTS); + } + + if($mixedWriteResult === false) + return $this->_includeFiles($sType, $aFiles, $bWrap); + + if($this->{'_b' . $sUpcaseType . 'Archive'}) + $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName); + + return $bWrap ? $this->$sMethodWrap($sCacheAbsoluteUrl) : $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, $bWrap = true) + { + $sUpcaseType = ucfirst($sType); + + $sMethodWrap = '_wrapInTag' . $sUpcaseType; + $sMethodLess = '_less' . $sUpcaseType; + + $mixedResult = $bWrap ? "" : []; + foreach($aFiles as $aFile) { + if($this->{'_b' . $sUpcaseType . 'Less'}) + $aFile = $this->$sMethodLess($aFile); + + $sFileUrl = $aFile['url']; + if(!$this->{'_b' . $sUpcaseType . 'Cache'}) + $sFileUrl = $this->addRevision($sFileUrl); + + if($bWrap) + $mixedResult .= $this->$sMethodWrap($sFileUrl); + else + $mixedResult[] = $sFileUrl; + } + + return $mixedResult; + } + /** + * 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) { + $mixedFile = $this->_locateFile($sType, $sFile); + if($mixedFile === false) + continue; + + list($sUrl, $sPath) = $mixedFile; + + $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled'); + switch($sAction) { + case 'add': + /** + * @hooks + * @hookdef hook-system-add_files 'system', 'add_files' - hook on add file to page + * - $unit_name - equals `system` + * - $action - equals `add_files` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `file` - [string] file name + * - `type` - [string] file type + * - `dynamic` - [bool] true if added as dynamic + * - `system` - [bool] true if system file + * - `url` - [string] by ref, file url, can be overridden in hook processing + * - `path` - [string] by ref, file path, can be overridden in hook processing + * @hook @ref hook-system-add_files + */ + bx_alert('system', 'add_files', 0, 0, [ + 'file' => $sFile, + 'type' => $sType, + 'dynamic' => $bDynamic, + 'system' => $bSystem, + 'url' => &$sUrl, + 'path' => &$sPath, + ]); + + if($bDynamic) + $sResult .= $this->$sMethodWrap($this->addRevision($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; + } + + function _locateFile($sType, $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 { + $sMethodLocate = '_getAbsoluteLocation' . ucfirst($sType); + + $sUrl = $this->$sMethodLocate('url', $sFile); + $sPath = $this->$sMethodLocate('path', $sFile); + } + + return !empty($sUrl) && !empty($sPath) ? [$sUrl, $sPath] : false; + } + + /** + * Parse content. + * + * @param string $sContent - HTML file's content. + * @param array $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix. + * @param mixed $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise). + * @return string the result of operation. + */ + function _parseContent($sContent, $aVariables, $mixedKeyWrapperHtml = null) + { + $aKeysSrc = array_keys($aVariables); + $aValuesSrc = array_values($aVariables); + + $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml); + + $sKeyIf = 'bx_if:'; + $sKeyRepeat = 'bx_repeat:'; + $iCountKeys = count($aKeysSrc); + $aKeys = $aValues = array(); + + //--- Parse simple keys ---// + for ($i = 0; $i < $iCountKeys; $i++) { + if (strncmp($aKeysSrc[$i], $sKeyRepeat, 10) === 0 || strncmp($aKeysSrc[$i], $sKeyIf, 6) === 0) + continue; + + $aKeys[] = "'" . $aKeyWrappers['left'] . $aKeysSrc[$i] . $aKeyWrappers['right'] . "'s"; + if (is_string($aValuesSrc[$i]) || is_null($aValuesSrc[$i])) + $aValues[] = is_null($aValuesSrc[$i]) ? '' : str_replace('$', '\\$', str_replace('\\', '\\\\', $aValuesSrc[$i])); + else if(is_array($aValuesSrc[$i])) + $aValues[] = _t('_error occured'); + else + $aValues[] = $aValuesSrc[$i]; + } + + //--- Parse keys with constructions ---// + for ($i = 0; $i < $iCountKeys; $i++) { + if (strncmp($aKeysSrc[$i], $sKeyRepeat, 10) === 0) { + $sKey = "'<" . $aKeysSrc[$i] . ">(.*)<\/" . $aKeysSrc[$i] . ">'s"; + + $aMatches = array(); + preg_match($sKey, $sContent, $aMatches); + + $sValue = ''; + if(isset($aMatches[1]) && !empty($aMatches[1])) { + if(is_array($aValuesSrc[$i])) + foreach($aValuesSrc[$i] as $aValue) + if(is_array($aValue)) + $sValue .= $this->parseHtmlByContent($aMatches[1], $aValue, $mixedKeyWrapperHtml); + else if(is_string($aValue)) + $sValue .= $aValue; + else if(is_string($aValuesSrc[$i])) + $sValue = $aValuesSrc[$i]; + } + } + else if (strncmp($aKeysSrc[$i], $sKeyIf, 6) === 0) { + $sKey = "'<" . $aKeysSrc[$i] . ">(.*)<\/" . $aKeysSrc[$i] . ">'s"; + + $aMatches = array(); + preg_match($sKey, $sContent, $aMatches); + + $sValue = ''; + if(isset($aMatches[1]) && !empty($aMatches[1])) + if(is_array($aValuesSrc[$i]) && isset($aValuesSrc[$i]['content']) && isset($aValuesSrc[$i]['condition']) && $aValuesSrc[$i]['condition']) + $sValue .= $this->parseHtmlByContent($aMatches[1], $aValuesSrc[$i]['content'], $mixedKeyWrapperHtml); + } + else + continue; + + $aKeys[] = $sKey; + $aValues[] = str_replace('$', '\\$', str_replace('\\', '\\\\', $sValue)); + } + + try { + $oTemplate = &$this; + + $aCallbackPatterns = array( + "''s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_text'), + "''s" => BX_DOL_TEMPLATE_CHECK_IN_BASE, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_text'), + "''s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_text') + ); + 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( + "''s" => 'get_menu', + )); + } + catch(Exception $oException) { + bx_log('sys_template', "Error in _parseContent method. Cannot parse template insertion ().\n" . + " Error ({$oException->getCode()}): {$oException->getMessage()}\n" . + (getLoggedId() ? " Account ID: " . getLoggedId() . "\n" : "") + ); + + return ''; + } + + $aKeys = array_merge($aKeys, array( + "''", + "''", + )); + $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 ---// + try { + $sContent = preg_replace_callback("'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", function($aMatches) use($oTemplate, $mixedKeyWrapperHtml) { + return $oTemplate->parseSystemKey($aMatches[1], $mixedKeyWrapperHtml); + }, $sContent); + } + catch(Exception $oException) { + bx_log('sys_template', "Error in _parseContent method. Cannot parse System Keys.\n" . + " Error ({$oException->getCode()}): {$oException->getMessage()}\n" . + (getLoggedId() ? " Account ID: " . getLoggedId() . "\n" : "") + ); + + return ''; + } + + 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++) { + $sVarNameKey = $aVarName . "['" . $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 .= ""; + if(($sInnerValue = $this->_compileContent($aMatches[1], $sVarNameKey . "[" . $sIndex . "]", $iVarDepth + 1, current($aValues[$i]), $mixedKeyWrapperHtml)) === false) + return false; + + $sValue .= $sInnerValue; + $sValue .= ""; + } + } + 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]) || empty($aValues[$i]['content']) || !is_array($aValues[$i]['content'])) + return false; + + $sValue .= ""; + if(($sInnerValue = $this->_compileContent($aMatches[1], $sVarNameKey . "['content']", $iVarDepth, $aValues[$i]['content'], $mixedKeyWrapperHtml)) === false) + return false; + + $sValue .= $sInnerValue; + $sValue .= ""; + } + } + else { + $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s"; + $sValue = ""; + } + + $aKeys[$i] = $sKey; + $aValues[$i] = $sValue; + } + + try { + $oTemplate = &$this; + + $aCallbackPatterns = array( + "''s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_text'), + "''s" => BX_DOL_TEMPLATE_CHECK_IN_BASE, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_text'), + "''s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL, + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_general'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_profile'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_group'), + "''s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_text') + ); + 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 ({$aMatches[1]}).", 1); + + return $mixedResult; + }, $sContent); + + $sContent = $this->_parseContentKeys($sContent); + } + catch(Exception $oException) { + if(($iCode = $oException->getCode()) != 1) + bx_log('sys_template', "Error in _compileContent method. Cannot parse template insertion ().\n" . + " Error ({$iCode}): {$oException->getMessage()}\n" . + (getLoggedId() ? " Account ID: " . getLoggedId() . "\n" : "") + ); + + return false; + } + + $aKeys = array_merge($aKeys, array( + "''s", + "''", + "''" + )); + $aValues = array_merge($aValues, array( + "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'] . "'", "parseSystemKey('\\1', \$mixedKeyWrapperHtml);?>", $sContent); + + return $sContent; + } + protected function _parseContentKeys($sContent, $aCallbackPatterns = array()) + { + $oTemplate = &$this; + + $aCallbackPatterns = array_merge($aCallbackPatterns, array( + "''s" => "get_image_auto", + "''s" => "get_image_url", + "''s" => "get_icon_url", + "''s" => "get_text", + "''s" => "get_text_js", + "''s" => "get_text_attribute", + "''s" => "get_page", + )); + + foreach($aCallbackPatterns as $sPattern => $sAction) + $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $sAction) { + $sResult = ''; + + switch($sAction) { + case 'get_image_auto': + $sResult = $oTemplate->getImageAuto($aMatches[1]); + break; + 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; + case 'get_page': + $oPage = BxDolPage::getObjectInstanceByURI($aMatches[1], false, true); + $oPage->setSubPage(true); + $sResult = $oPage ? $oPage->getCode() : ''; + 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]); + } + } + + /** + * Module(mod) related locations will be checked first in TMPL and BASE, + * then system(sys) location(s) will be checked in TMPL and BASE. + */ + $aLocationsList = array_reverse($this->_aLocations, true); + $aLocationsGrouped = ['mod' => [], 'sys' => []]; + foreach($aLocationsList as $sLocation => $aLocation) { + if(in_array($sLocation, ['system', 'studio'])) + $aLocationsGrouped['sys'][$sLocation] = $aLocation; + else + $aLocationsGrouped['mod'][$sLocation] = $aLocation; + } + + $sResult = ''; + foreach($aLocationsGrouped as $aLocations) { + //--- Check it Template. + $bInSub = false; + $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_TMPL]; + if(in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn))) + foreach($aLocations as $sKey => $aLocation) + if((!$bInSub || $sCheckIn['sub'] == $sKey) && 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; + break 2; + } + + //--- Check it Base. + $bInSub = false; + $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_BASE]; + if(empty($sResult) && (in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn)))) + foreach($aLocations as $sKey => $aLocation) + if((!$bInSub || $sCheckIn['sub'] == $sKey) && extFileExists($aLocation['path'] . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sFolder . $sName)) { + $sResult = $aLocation[$sType] . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sFolder . $sName; + break 2; + } + } + + /** + * try to find from received path + */ + if(!$sResult && @is_file(BX_DIRECTORY_PATH_ROOT . $aParts[0] . DIRECTORY_SEPARATOR . $aParts[1])) { + $sResult = $sRoot . $aParts[0] . $sDivider . $aParts[1]; + } + + if(isset($sLocationKey)) + $this->removeLocation($sLocationKey); + + return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult; + } + /** + * Get absolute location of some template's part. + * + * @param string $sType result type. Available values 'url' and 'path'. + * @param string $sName requested part name. + * @return string absolute location (path/url) of the part. + */ + function _getAbsoluteLocationJs($sType, $sName) + { + $sResult = ''; + $aLocations = array_reverse($this->_aLocationsJs, true); + foreach($aLocations as $sKey => $aLocation) { + if(extFileExists($aLocation['path'] . $sName)) + $sResult = $aLocation[$sType] . $sName; + else + continue; + break; + } + return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult; + } + function _getAbsoluteLocationCss($sType, $sName) + { + $sNameLess = str_replace('.css', '.less', $sName); + + $sResult = $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sNameLess); + if(!empty($sResult)) + return $sResult; + + return $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sName); + } + /** + * Get inline data for Images and Icons. + * + * @param string $sType image/icon + * @param string $sName file name + * @param string $sCheckIn where the content would be searched(base, template, both) + * @return unknown + */ + function _getInlineData($sType, $sName, $sCheckIn) + { + switch($sType) { + case 'image': + $sFolder = $this->_sFolderImages; + break; + case 'icon': + $sFolder = $this->_sFolderIcons; + break; + } + $sPath = $this->_getAbsoluteLocation('path', $sFolder, $sName, $sCheckIn); + + $iFileSize = 0; + if($this->_bImagesInline && ($iFileSize = filesize($sPath)) !== false && $iFileSize < $this->_iImagesMaxSize) { + $aFileInfo = pathinfo($sPath); + return $this->getImageMimeType($aFileInfo['extension']) . ";base64," . base64_encode(file_get_contents($sPath)); + } + + return false; + } + + /** + * Get file name where the template would be cached. + * + * @param string $sAbsolutePath template's real path. + * @return string the result of operation. + */ + function _getCacheFileName($sType, $sAbsolutePath) + { + $sResult = bx_site_hash($sAbsolutePath); + switch($sType) { + case 'html': + $sResult = $this->_sCacheFilePrefix . bx_lang_name() . '_' . $this->_sCode . '_' . $sResult; + break; + case 'css': + $sResult = $this->_sCssCachePrefix . (!empty($this->_iMix) ? $this->_iMix . '_' : '') . $sResult; + break; + case 'js': + $sResult = $this->_sJsCachePrefix . $sResult; + break; + } + + return $sResult; + } + /** + * Get template key wrappers(left, right) + * + * @param mixed $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise). + * @return array result of operation. + */ + function _getKeyWrappers($mixedKeyWrapperHtml) + { + $aResult = array(); + if(!empty($mixedKeyWrapperHtml) && is_string($mixedKeyWrapperHtml)) + $aResult = array('left' => $mixedKeyWrapperHtml, 'right' => $mixedKeyWrapperHtml); + else if(!empty($mixedKeyWrapperHtml) && is_array($mixedKeyWrapperHtml)) + $aResult = array('left' => $mixedKeyWrapperHtml[0], 'right' => $mixedKeyWrapperHtml[1]); + else + $aResult = array('left' => $this->_sKeyWrapperHtml, 'right' => $this->_sKeyWrapperHtml); + return $aResult; + } + + /** + * Process all added language translations and return them as a string. + * + * @return string with JS code. + */ + function _processJsTranslations() + { + $sReturn = ''; + if(isset($this->aPage['js_translations']) && is_array($this->aPage['js_translations'])) { + foreach($this->aPage['js_translations'] as $sKey => $sString) + $sReturn .= "'" . bx_js_string($sKey) . "': '" . bx_js_string($sString) . "',"; + + $sReturn = substr($sReturn, 0, -1); + } + + return ' +'; + } + /** + * 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 ''; + } + /** + * 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 ''; + } + + /** + * 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; + } + + /** + * Get current revision number. + * + * @return integer number + */ + public function getRevision() + { + return (int)getParam('sys_revision'); + } + + /** + * Add current revision number to URL. + * + * @return string with URL + */ + public function addRevision($sUrl) + { + return bx_append_url_params($sUrl, ['rev' => $this->getRevision()]); + } + + /** + * + * Functions to display pages with errors, messages and so on. + * + */ + function displayAccessDenied ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF) + { + bx_import('BxDolLanguages'); + header('HTTP/1.0 403 Forbidden'); + header('Status: 403 Forbidden'); + + $a = [ + 'title' => _t('_access_denied_page_title'), + 'content' => _t('_access_denied_page_content'), + ]; + + $this->displayMsg($sMsg ? $sMsg : _t('_Access denied'), false, $iPage, $iDesignBox); + } + + function displayNoData ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF) + { + bx_import('BxDolLanguages'); + header('HTTP/1.0 204 No Content'); + header('Status: 204 No Content'); + $this->displayMsg($sMsg ? $sMsg : _t('_Empty'), false, $iPage, $iDesignBox); + } + + function displayErrorOccured ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF) + { + bx_import('BxDolLanguages'); + header('HTTP/1.0 500 Internal Server Error'); + header('Status: 500 Internal Server Error'); + $this->displayMsg($sMsg ? $sMsg : _t('_error occured'), false, $iPage, $iDesignBox); + } + + function displayPageNotFound ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF) + { + bx_import('BxDolLanguages'); + header('HTTP/1.0 404 Not Found'); + header('Status: 404 Not Found'); + $this->displayMsg($sMsg ? $sMsg : _t('_sys_request_page_not_found_cpt'), false, $iPage, $iDesignBox); + } + + function displayMsg ($s, $bTranslate = false, $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF) + { + $sError = '_Error'; + $bArray = is_array($s); + + $sTitle = $bArray ? $s['title'] : ($bTranslate ? $sError : _t($sError)); + $sContent = $bArray ? $s['content'] : $s; + + if($bTranslate) { + $sTitle = _t($sTitle); + $sContent = _t($sContent); + } + + if (bx_is_api()) + return [bx_api_get_msg($sContent)]; + + $sContent = MsgBox($sContent); + if($iPage == BX_PAGE_DEFAULT) + $sContent = DesignBoxContent($sTitle, $sContent, $iDesignBox); + + $oTemplate = BxDolTemplate::getInstance(); + $oTemplate->setPageNameIndex ($iPage); + $oTemplate->setPageHeader ($sTitle); + $oTemplate->setPageContent ('page_main_code', $sContent); + $oTemplate->getPageCode(); + exit; + } + + /** + * * * * Static methods for work with template injections * * * + * + * Static method is used to add/replace the content of some key in the template. + * It's usefull when you don't want to modify existing template but need to add some data to existing template key. + * + * @param integer $iPageIndex - page index where injections would processed. Use 0 if you want it to be done on all the pages. + * @param string $sKey - template key. + * @param string $sValue - the data to be added. + * @return string the result of operation. + */ + function processInjection($iPageIndex, $sKey, $sValue = "") + { + if($iPageIndex != 0 && isset($this->aPage['injections']['page_0'][$sKey]) && isset($this->aPage['injections']['page_' . $iPageIndex][$sKey])) + $aSelection = @array_merge($this->aPage['injections']['page_0'][$sKey], $this->aPage['injections']['page_' . $iPageIndex][$sKey]); + else if(isset($this->aPage['injections']['page_0'][$sKey])) + $aSelection = $this->aPage['injections']['page_0'][$sKey]; + else if(isset($this->aPage['injections']['page_' . $iPageIndex][$sKey])) + $aSelection = $this->aPage['injections']['page_' . $iPageIndex][$sKey]; + else + $aSelection = array(); + + if(is_array($aSelection)) + foreach($aSelection as $aInjection) { + + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginInjection($sRand = time().rand()); + + $sInjData = ''; + switch($aInjection['type']) { + case 'text': + $sInjData = $aInjection['data']; + break; + + case 'service': + if(BxDolService::isSerializedService($aInjection['data'])) + $sInjData = BxDolService::callSerialized($aInjection['data']); + break; + } + + if((int)$aInjection['replace'] == 1) + $sValue = $sInjData; + else + $sValue .= $sInjData; + + if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endInjection($sRand, $aInjection); + + } + + return $sValue != '__' . $sKey . '__' ? str_replace('__' . $sKey . '__', '', $sValue) : $sValue; + } + /** + * Static method to add ingection available on the current page only. + * + * @param string $sKey - template's key. + * @param string $sType - injection type(text, php). + * @param string $sData - the data to be added. + * @param integer $iReplace - replace already existed data or not. + */ + function addInjection($sKey, $sType, $sData, $iReplace = 0) + { + $this->aPage['injections']['page_0'][$sKey][] = array( + 'page_index' => 0, + 'key' => $sKey, + 'type' => $sType, + 'data' => $sData, + 'replace' => $iReplace + ); + } + + function getPageCode($oTemplate = null) + { + if (empty($oTemplate)) + $oTemplate = $this; + + /** + * @hooks + * @hookdef hook-system-design_before_output 'system', 'design_before_output' - hook on before page's html generated + * - $unit_name - equals `system` + * - $action - equals `design_before_output` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `page` - [array] by ref, page object paramenters, can be overridden in hook processing + * - `page_content` - [array] by ref, page content values, can be overridden in hook processing + * @hook @ref hook-system-design_before_output + */ + bx_alert('system', 'design_before_output', 0, 0, ['page' => &$this->aPage, 'page_content' => &$this->aPageContent]); + + header( 'Content-type: text/html; charset=utf-8' ); + header( 'X-Frame-Options: sameorigin' ); + if (BX_PAGE_EMBED == $oTemplate->getPageNameIndex()) + header('Content-Security-Policy: frame-ancestors ' . getParam('sys_csp_frame_ancestors')); + + $sResult = $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent()); + + /** + * @hooks + * @hookdef hook-system-design_after_output 'system', 'design_after_output' - hook on after page's html generated + * - $unit_name - equals `system` + * - $action - equals `design_after_output` + * - $object_id - not used + * - $sender_id - not used + * - $extra_params - array of additional params with the following array keys: + * - `override_result` - [string] by ref, html content for current page, can be overridden in hook processing + * @hook @ref hook-system-design_after_output + */ + bx_alert('system', 'design_after_output', 0, false, ['override_result' => &$sResult]); + + echo $sResult; + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTwilio.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTwilio.php new file mode 100644 index 0000000000..c0ab4cc167 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolTwilio.php @@ -0,0 +1,78 @@ +_sSid = getParam('sys_twilio_gate_sid'); + $this->_sToken = getParam('sys_twilio_gate_token'); + $this->_sFromNumber = getParam('sys_twilio_gate_from_number'); + } + + /** + * Prevent cloning the instance + */ + public function __clone() + { + if (isset($GLOBALS['bxDolClasses'][get_class($this)])) + trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR); + } + + /** + * Get singleton instance of the class + */ + public static function getInstance() + { + if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) + $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolTwilio(); + + return $GLOBALS['bxDolClasses'][__CLASS__]; + } + + public function sendSms($sTo, $sMessage, $sFrom = '') + { + try { + $client = new Twilio\Rest\Client($this->_sSid, $this->_sToken); + $aParams = array('body' => $sMessage, 'from' => $sFrom != '' ? $this->normalizePhone($sFrom) : $this->normalizePhone($this->_sFromNumber)); + $client->messages->create($this->normalizePhone($sTo), $aParams); + return true; + } + catch (Exception $oException) { + $this->writeLog($oException->getFile() . ':' . $oException->getLine() . ' ' . $oException->getMessage()); + return false; + } + } + + public function normalizePhone($sPhone){ + $sPhone = trim($sPhone); + if (substr($sPhone, 0, 1) != '+'){ + $sPhone = '+' . $sPhone; + } + return $sPhone; + } + + private function writeLog($sString) + { + bx_log('sys_twilio', $sString); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolUploader.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolUploader.php new file mode 100644 index 0000000000..6c898c17aa --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolUploader.php @@ -0,0 +1,562 @@ + 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 + * + * @endcode + * + * + * 2. Pass form array - regular form array, but with inputs array only, for example: + * + * @code + * array ( + * 'inputs' => array ( + * 'file_name' => array ( + * 'type' => 'text', + * 'name' => 'file_name[]', + * 'value' => '{file_title}', + * 'caption' => _t('Caption'), + * ), + * 'file_desc' => array ( + * 'type' => 'textarea', + * 'name' => 'file_desc[]', + * 'caption' => _t('Description'), + * ), + * ), + * ); + * @endcode + * + * Array is automatically modified to add necessary form attributes to work as nested form, + * file id field is added automatically as hidden input as well. + * + * + * 3. Pass instance of BxDolFormNestedGhost class - use BxDolFormNestedGhost class or its custom subclass; + * to create instance use the same form array as in the previous variant, for example: + * + * @code + * $oFormNested = new BxDolFormNestedGhost('attachment', $aFormNested, 'do_submit'); + * @endcode + * + * - 'attachment' is the name of file form field from main form. + * - $aFormNested is form array from previous example. + * - 'do_submit' is main form submit_name; field name of submit form input to determine if form is submitted or not. + * + * + * All 3 variants can have the following replace markers to substitute with real values: + * - {file_id} - uploaded file id + * - {file_name} - uploaded file name with extension + * - {file_title} - uploaded file name without extension + * - {file_icon} - URL to file icon automatically determined by file extension + * - {file_url} - URL to the original file + * - {js_instance_name} - instance of BxDolUploader javascript class + * + */ +abstract class BxDolUploader extends BxDolFactory +{ + protected $_oTemplate; + + protected $_aObject; ///< object properties + protected $_sStorageObject; ///< storage object name + + protected $_sUniqId; ///< uniq id used to generate UploaderJsInstance, ResultContainerId, UploadInProgressContainerId and PopupContainerId + protected $_sUploaderJsInstance; ///< uplooader js object instance name + protected $_sUploadInProgressContainerId; ///< container id where upload in progress element resides + protected $_sPopupContainerId; ///< popup container id + + protected $_sResultContainerId; ///< uploading/uploaded objects container id + protected $_sErrorsContainerId; + protected $_sFormContainerId; + + protected $_sUploadErrorMessages; ///< upload error message + + protected $_sButtonTemplate; ///< template name for displaying upload button + protected $_sJsTemplate; ///< template name for displaying upload JS + protected $_sUploaderFormTemplate; ///< template name for displaying uploader form + + protected $_aJs; + protected $_aCss; + + /** + * 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'; + + $this->_aJs = ['BxDolUploader.js']; + $this->_aCss = ['uploaders.css']; + } + + 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 + */ + public 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); + + + if (bx_is_api() && $_FILES['file']) { + $iId = $oStorage->storeFileFromForm($_FILES['file'], $bPrivate, $iProfileId, $iContentId); + $aResponse = array ('success' => 1, 'id' => $iId); + return $aResponse; + } + + 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 ''; + } + + public function getUploadErrorMessages ($sFormat = 'HTML') + { + if (!$this->_sUploadErrorMessages) + return ''; + + if ('HTML' == $sFormat) + return nl2br($this->_sUploadErrorMessages); + else + return $this->_sUploadErrorMessages; + } + + /** + * Show uploader button. + * @return HTML string + */ + public function getUploaderButton($aParams = array()) + { + return $this->_oTemplate->parseHtmlByName($this->_sButtonTemplate, $aParams); + } + + public function getUploaderJsParams() + { + return []; + } + + /** + * Show uploader JS. + * @return HTML string + */ + public function getUploaderJs($mixedGhostTemplate, $isMultiple = true, $aParams = array(), $bDynamic = false) + { + $sJsValue = ''; + if(is_array($mixedGhostTemplate)) + $sJsValue = json_encode($mixedGhostTemplate); + else + $sJsValue = "'" . bx_js_string($mixedGhostTemplate, BX_ESCAPE_STR_APOS) . "'"; + + $sJsObject = $this->getNameJsInstanceUploader(); + $sJsCode = $this->_oTemplate->parseHtmlByName($this->_sJsTemplate, array_merge([ + 'uploader_instance_name' => $sJsObject, + 'engine' => $this->_aObject['object'], + 'storage_object' => $this->_sStorageObject, + 'images_transcoder' => '', + 'uniq_id' => $this->_sUniqId, + 'template_ghost' => $sJsValue, + 'multiple' => $isMultiple ? 1 : 0, + 'latest' => 0, //--- Return the latest one uploaded ghost only. + 'storage_private' => isset($aParams['storage_private']) ? $aParams['storage_private'] : 1, + 'is_init_reordering' => isset($aParams['is_init_reordering']) ? $aParams['is_init_reordering'] : 0, + 'bx_if:restore_ghosts' => [ + 'condition' => isset($aParams['is_init_ghosts']) ? $aParams['is_init_ghosts'] : 1, + 'content' => [ + 'uploader_instance_name' => $sJsObject, + 'is_init_reordering' => isset($aParams['is_init_reordering']) ? $aParams['is_init_reordering'] : 0, + ] + ], + 'on_upload_before' => 'false', + 'on_upload' => 'false', + 'on_restore_ghosts' => 'false' + ], $aParams)); + + if(!$bDynamic) { + $this->_oTemplate->addJs($this->_aJs); + $sJsCode = $this->_oTemplate->addJsCodeOnLoadWrapped($sJsCode); + } + else + $sJsCode = $this->_oTemplate->addJsPreloadedWrapped($this->_aJs, $sJsCode); + + return $this->addCssJs($bDynamic) . $sJsCode; + } + + /** + * add necessary js, css files and js translations + */ + public function addCssJs($bDynamic = false) + { + $s = ''; + $s .= $this->_oTemplate->addCss($this->_aCss, $bDynamic); + $s .= $this->_oTemplate->addJsTranslation([ + '_sys_uploader_confirm_leaving_page', + '_sys_uploader_confirm_close_popup', + '_sys_uploader_upload_canceled', + '_sys_uploader_image_reposition_info', + ], $bDynamic); + return $bDynamic ? $s : ''; + } + + public function addJs($mixedFile) + { + if(!is_array($mixedFile)) + $mixedFile = [$mixedFile]; + + foreach($mixedFile as $sFile) + if(!in_array($sFile, $this->_aJs)) + $this->_aJs[] = $sFile; + } + + public function addCss($mixedFile) + { + if(!is_array($mixedFile)) + $mixedFile = [$mixedFile]; + + foreach($mixedFile as $sFile) + if(!in_array($sFile, $this->_aCss)) + $this->_aCss[] = $sFile; + } + + /** + * Get uploader button title + */ + public function getUploaderButtonTitle($mixed = false) + { + // it is overrided in child classes + } + + /** + * Show uploader form. + * @return HTML string + */ + public function getUploaderForm($isMultiple = true, $iContentId = false, $isPrivate = true) + { + // it is overrided in child classes + } + + /** + * Display uploaded, but not saved files - ghosts + * @param $iProfileId - profile id to get orphaned files from + * @param $sFormat - output format, only 'json' output formt is supported + * @param $sImagesTranscoder - transcoder object for files preview for images and videos, false by default - no preview + * @param $iContentId - content id to get orphaned files from, false by default + * @return JSON string + */ + public function getGhosts($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false) + { + $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject); + + $oImagesTranscoder = false; + if ($sImagesTranscoder) + $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($sImagesTranscoder); + + $a = array(); + $aGhosts = $oStorage->getGhosts($this->isAdmin($iContentId) && $iContentId ? false : $iProfileId, $iContentId); + foreach ($aGhosts as $aFile) { + $sFileIcon = ''; + + if ($this->isUseTranscoderForPreview($oImagesTranscoder, $aFile)) + $sFileIcon = $oImagesTranscoder->getFileUrl($aFile['id']); + + if (!$sFileIcon) + $sFileIcon = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name'])); + + $aVars = array ( + 'storage_object' => $this->_sStorageObject, + 'file_id' => $aFile['id'], + 'file_type' => $aFile['mime_type'], + 'file_name' => $aFile['file_name'], + 'file_title' => $oStorage->getFileTitle($aFile['file_name']), + 'file_icon' => $sFileIcon, + 'file_url' => $oStorage->getFileUrlById($aFile['id']), + 'file_remote_id' => $aFile['remote_id'], + 'js_instance_name' => $this->_sUploaderJsInstance, + ); + + $a[$aFile['id']] = array_merge($aVars, $this->getGhostTemplateVars($aFile, $iProfileId, $iContentId, $oStorage, $oImagesTranscoder)); + } + + if ('array' == $sFormat) { + return $a; + } + else if ('json' == $sFormat) { + return json_encode($a); + } else { // html format is not suported for this data type + return false; + } + } + + public function getGhostsWithOrder($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false, $isLatestOnly = false) + { + $a = $this->getGhosts($iProfileId, 'array', $sImagesTranscoder, $iContentId); + if($isLatestOnly) + $a = array_slice($a, 0, 1, true); + + if(!empty($a) && is_array($a)) + $a = ['g' => $a, 'o' => array_keys($a)]; + + if ('json' == $sFormat) { + return json_encode($a); + } else { // html format is not suported for this data type + return $a; + } + } + + /** + * Reorder uploaded ghosts. + * @param $iProfileId - profile id to get orphaned files from + * @param $sFormat - output format, only 'json' output formt is supported + * @param $aGhosts - an array of ordered ghosts' IDs. + * @param $iContentId - content id to order orphaned files for, false by default + * @return JSON string + */ + public function reorderGhosts($iProfileId, $sFormat, $aGhosts, $iContentId = false) + { + $bResult = true; + if(($oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject)) !== false) + $bResult = $oStorage->reorderGhosts($this->isAdmin($iContentId) && $iContentId ? false : $iProfileId, $iContentId, $aGhosts); + + if($sFormat == 'json') + return json_encode($bResult ? [] : ['msg' => _t('_error occured')]); + else + return $bResult; + } + + /** + * Delete file by file id, usually ghost file + * @return 'ok' string on success or error string on error + */ + public function deleteGhost($iFileId, $iProfileId) + { + $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject); + + $aFile = $oStorage->getGhost ($iFileId); + if (!$aFile) + $aFile = $oStorage->getFile ($iFileId); + if (!$aFile) + return _t('_error occured'); + + $oProfile = BxDolProfile::getInstance($iProfileId); + $oAccount = $oProfile ? $oProfile->getAccountObject() : null; + $aProfiles = $oAccount ? $oAccount->getProfiles(false) : array(); + if (!isset($aProfiles[$aFile['profile_id']]) && !$this->isAdmin($aFile['content_id'])) + return _t('_sys_txt_access_denied'); + + if (!$oStorage->deleteFile($iFileId)) + return $oStorage->getErrorString(); + + return 'ok'; + } + + /** + * Delete all ghosts files for the specified profile + * @return number of delete ghost files + */ + public function deleteGhostsForProfile($iProfileId, $iContentId = false) + { + $iCount = 0; + + $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject); + + $aGhosts = $oStorage->getGhosts($iProfileId, $iContentId, $iContentId ? true : false); + foreach ($aGhosts as $aFile) + $iCount += $oStorage->deleteFile($aFile['id']); + + return $iCount; + } + + protected function cleanUploadErrorMessages () + { + $this->_sUploadErrorMessages = ''; + } + + public 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 .= '
' . $s . '
'; + + return $sTextRestrictions; + } + + protected function getMaxUploadFileSize () + { + $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject); + if (!$oStorage) + return 0; + return $oStorage->getMaxUploadFileSize(bx_get_logged_profile_id()); + } + + protected function getAcceptedFilesExtensions () + { + $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject); + if (!$oStorage) + return null; + return $oStorage->getAllowedExtensions(); + } + + protected function getGhostTemplateVars($aFile, $iProfileId, $iContentId, $oStorage, $oImagesTranscoder) + { + return array(); + } + + protected function isUseTranscoderForPreview($oImagesTranscoder, $aFile) + { + if (!$oImagesTranscoder) + return false; + + return $oImagesTranscoder->isMimeTypeSupported($aFile['mime_type']); + } + + protected function isAdmin ($iContentId = 0) + { + return isAdmin(); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolVoteReactions.php b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolVoteReactions.php new file mode 100644 index 0000000000..22b4ce89fc --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/classes/BxDolVoteReactions.php @@ -0,0 +1,333 @@ +_oQuery = new BxDolVoteReactionsQuery($this); + $this->_sType = BX_DOL_VOTE_TYPE_REACTIONS; + + $this->_sMenuDoVote = 'sys_vote_reactions_do'; + + $this->_sDataList = 'sys_vote_reactions'; + $this->_aDataList = array(); + + $this->_sDefault = 'default'; + + $this->_bQuickMode = getParam('sys_vote_reactions_quick_mode') == 'on'; + } + + public function init($iId) + { + if(!parent::init($iId)) + return false; + + $aReactions = BxDolFormQuery::getDataItems($this->_sDataList, false, BX_DATA_VALUES_ALL); + + $sDefault = ''; + foreach($aReactions as $sReaction => $aReaction) { + $aData = !empty($aReaction['Data']) ? unserialize($aReaction['Data']) : array(); + + if(!empty($aData['default'])) + $sDefault = $sReaction; + + $this->_aDataList[$sReaction] = array( + 'name' => $sReaction, + 'title' => $aReaction['LKey'], + 'title_aux' => $aReaction['LKey2'], + 'use' => isset($aData['use']) ? $aData['use'] : 'emoji', + 'icon' => isset($aData['icon']) ? $aData['icon'] : '', + 'emoji' => isset($aData['emoji']) ? $aData['emoji'] : '', + 'image' => isset($aData['image']) ? $aData['image'] : '', + 'color' => isset($aData['color']) ? $aData['color'] : '', + 'weight' => isset($aData['weight']) ? $aData['weight'] : 1, + 'default' => isset($aData['default']) ? $aData['default'] : '', + ); + } + + if(empty($sDefault) && !empty($this->_aDataList)) + $sDefault = current(array_keys($this->_aDataList)); + + if(!empty($sDefault)) { + $aDefault = $this->_aDataList[$sDefault]; + if((!$this->_bQuickMode || $this->_bApi) && isset($aDefault['default'])) { + if(is_array($aDefault['default'])) + $aDefault = array_merge ($aDefault, $aDefault['default']); + else + $aDefault['icon'] = $aDefault['default']; + } + + $this->_aDataList[$this->_sDefault] = $aDefault; + } + } + /** + * Interface functions for outer usage + */ + public function getValue() + { + return (int)$this->_aSystem['min_value']; + } + + public function getDefault() + { + return $this->_sDefault; + } + + public function getReaction($sName) + { + if(empty($this->_aDataList)) + $this->getReactions(); + + return !empty($this->_aDataList[$sName]) ? $this->_aDataList[$sName] : false; + } + + public function getReactions($bFullInfo = false) + { + return $bFullInfo ? $this->_aDataList : array_keys($this->_aDataList); + } + + public function getTrackBy($aParams) + { + return $this->_oQuery->getTrackBy($aParams); + } + + public function getIcon($sReaction, $bWithColor = true) + { + $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault]; + + return $aReaction['icon'] . ($bWithColor && !empty($aReaction['color']) ? ' ' . $aReaction['color'] : ''); + } + + public function getEmoji($sReaction, $bWithColor = true) + { + $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault]; + + return $aReaction['emoji']; + } + + public function getImage($sReaction, $bWithColor = true) + { + $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault]; + + list($sIconFont, $sIconUrl, $sIconA, $sIconHtml) = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIcon($aReaction['image']); + if ($sIconUrl) + return $this->_oTemplate->parseImage($sIconUrl, ['class' => 'sys-icon-image']); + + return $sIconHtml; + } + + /** + * Actions functions + */ + public function actionGetDoVotePopup() + { + if(!$this->isEnabled()) + return ''; + + return $this->_getDoVotePopup((int)bx_get('value')); + } + + public function actionGetVotedBy() + { + if(!$this->isEnabled()) + return ''; + + $aParams = array(); + + $sReaction = bx_get('reaction'); + if($sReaction !== false) { + $sReaction = bx_process_input($sReaction); + + $aReactions = $this->getReactions(); + if(!in_array($sReaction, $aReactions)) + $sReaction = $this->_sDefault; + + $aParams['reaction'] = $sReaction; + } + + return $this->_getVotedBy($aParams); + } + + /** + * Internal functions + */ + protected function _isVote($iObjectId = 0, $bForceGet = false) + { + $aVote = $this->_getVote($iObjectId, $bForceGet); + foreach($aVote as $sKey => $iValue) + if(strpos($sKey, 'count_') !== false && !empty($iValue)) + return true; + + return false; + } + + protected function _isDuplicate($iObjectId, $iAuthorId, $iAuthorIp, $bVoted) + { + return $bVoted && !$this->isUndo(); + } + + protected function _isCount($aVote = array()) + { + if(empty($aVote)) + $aVote = $this->_getVote(); + + foreach($aVote as $sKey => $mixedValue) + if(substr($sKey, 0, 5) == 'count' && (int)$mixedValue != 0) + return true; + + return false; + } + + protected function _getVoteData() + { + $aData = parent::_getVoteData(); + if($aData === false) + return false; + + $sReaction = bx_get('reaction'); + if($sReaction === false) + return false; + + $aData['reaction'] = bx_process_input($sReaction); + return $aData; + } + + protected function _returnVoteData($iObjectId, $iAuthorId, $iAuthorIp, $aData, $bVoted, $aParams = []) + { + $aReactions = $this->getReactions(true); + $sReaction = $aData['reaction']; + + $bUndo = $this->isUndo(); + $bDisabled = $bVoted && !$bUndo; + + $aVote = $this->_getVote($iObjectId, true); + $aTrack = $bVoted ? $this->_getTrack($iObjectId, $iAuthorId) : []; + + $sSwitchTo = $bVoted ? $sReaction : $this->_sDefault; + + $sLabelUse = !empty($aReactions[$sSwitchTo]['use']) ? $aReactions[$sSwitchTo]['use'] : 'emoji'; + $sLabelIcon = $this->_getIconDoWithTrack($bVoted, $aTrack); + if(!$bVoted && $sLabelUse != 'icon') { + $sLabelUse = 'icon'; + $sLabelIcon = $this->_oTemplate->parseIcon($sLabelIcon); + } + + $sJsClick = ''; + if(!$bDisabled) + $sJsClick = $bVoted && $bUndo ? $this->getJsClickDo($sReaction) : $this->getJsClick(); + + $iTotalC = $iTotalS = 0; + foreach(array_keys($aReactions) as $sName) { + $iTotalC += (int)$aVote['count_' . $sName]; + $iTotalS += (int)$aVote['sum_' . $sName]; + } + $fTotalR = $iTotalC != 0 ? round($iTotalS / $iTotalC, 2) : 0; + + $iCount = (int)$aVote['count_' . $sReaction]; + $aResult = [ + 'code' => 0, + 'reaction' => $this->_bApi ? $aReactions[$sSwitchTo]['name'] : $sReaction, + 'rate' => $aVote['rate_' . $sReaction], + 'count' => $iCount, + 'countf' => $iCount > 0 ? $this->_getCounterLabel($iCount, array('reaction' => $sReaction)) : '', + 'label_use' => $sLabelUse, + 'label_icon' => $sLabelIcon, + 'label_emoji' => $this->_getEmojiDoWithTrack($bVoted, $aTrack), + 'label_image' => $this->_getImageDoWithTrack($bVoted, $aTrack), + 'label_title' => _t($this->_getTitleDoWithTrack($bVoted, $aTrack)), + 'label_click' => $sJsClick, + 'voted' => $bVoted, + 'disabled' => $bVoted && !$this->isUndo(), + 'total' => [ + 'rate' => $fTotalR, + 'count' => $iTotalC, + 'countf' => $iTotalC > 0 ? $this->_getCounterLabel($iTotalC, ['show_counter_label_icon' => false, 'reaction' => '']) : '', + ] + ]; + + $sDefault = $this->getDefault(); + $aDefault = $this->getReaction($sDefault); + $aDefaultInfo = $this->getReaction($aDefault['name']); + + $aResult['api'] = [ + 'performer_id' => $iAuthorId, + 'is_voted' => $aResult['voted'], + 'is_disabled' => $aResult['disabled'], + 'reaction' => $aResult['voted'] ? $aResult['reaction'] : $sDefault, + 'icon' => !empty($aResult['label_emoji']) ? $aResult['label_emoji'] : $aDefaultInfo['emoji'], + 'title' => !empty($aResult['label_title']) ? $aResult['label_title'] : '', + 'counter' => $this->getVote() + ]; + + return $aResult; + } + + protected function _returnVoteDataForSocket($aData, $aMask = []) + { + return parent::_returnVoteDataForSocket($aData, ['code', 'reaction', 'rate', 'count', 'countf', 'total', 'api']); + } + + protected function _getIconDoWithTrack($bVoted, $aTrack = []) + { + $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault; + + return $this->getIcon($sReaction); + } + + protected function _getEmojiDoWithTrack($bVoted, $aTrack = []) + { + $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault; + + return $this->getEmoji($sReaction); + } + + protected function _getImageDoWithTrack($bVoted, $aTrack = []) + { + $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault; + + return $this->getImage($sReaction); + } + + protected function _getTitleDoWithTrack($bVoted, $aTrack = []) + { + $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault; + + return $this->_aDataList[$sReaction]['title']; + } + + protected function _getTitleDoBy($aParams = []) + { + if(isset($aParams['show_counter_style']) && $aParams['show_counter_style'] == self::$_sCounterStyleCompound) + return _t('_vote_do_by_reactions'); + + $sReaction = !empty($aParams['reaction']) ? $aParams['reaction'] : $this->_sDefault; + return _t('_vote_do_by_x_reaction', _t($this->_aDataList[$sReaction]['title'])); + } +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolCmts.js b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolCmts.js new file mode 100644 index 0000000000..b35e54ffed --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolCmts.js @@ -0,0 +1,1151 @@ +/** + * Copyright (c) UNA, Inc - https://una.io + * MIT License - https://opensource.org/licenses/MIT + * + * @defgroup UnaCore UNA Core + * @{ + */ + +function BxDolCmts (options) { + this._sObjName = undefined == options.sObjName ? 'oCmts' : options.sObjName; // javascript object name, to run current object instance from onTimer + this._sActionsUrl = options.sRootUrl + 'cmts.php'; // actions url address + + this._sSystem = options.sSystem; // current comment system + this._iAuthorId = options.iAuthorId; // this comment's author ID. + this._iObjId = options.iObjId; // this object id comments + this._sBaseUrl = options.sBaseUrl; // base url to view comment's listing. + this._sSocket = options.sSocket === undefined ? this._sSystem : options.sSocket; + + this._iMinPostForm = undefined == options.iMinPostForm ? 0 : options.iMinPostForm; + this._sPostFormPosition = undefined == options.sPostFormPosition ? 'top' : options.sPostFormPosition; + this._sDisplayType = undefined == options.sDisplayType ? 'threaded' : options.sDisplayType; + this._iDisplayStructure = undefined == options.iDisplayStructure ? 0 : options.iDisplayStructure; + + this._sBrowseType = undefined == options.sBrowseType ? 'tail' : options.sBrowseType; + this._sBrowseFilter = undefined == options.sBrowseFilter ? 'all' : options.sBrowseFilter; + + this._sSP = options.sStylePrefix === undefined ? 'cmt' : options.sStylePrefix; + this._aParams = options.aParams === undefined ? false : options.aParams; + this._aHtmlIds = options.aHtmlIds; + this._sAnimationEffect = 'fade'; + this._iAnimationSpeed = 'slow'; + + this._oSavedTexts = {}; + this._sRootId = '#cmts-box-' + this._sSystem + '-' + this._iObjId; + + this._bLiveUpdatePaused = false; +} + +/*--- Main layout functionality ---*/ +BxDolCmts.prototype.cmtInit = function() +{ + var $this = this; + + $(document).ready(function() { + // init socket + if(window.oBxDolSockets !== undefined && $this._sSocket) + oBxDolSockets.subscribe($this._sSocket, $this._iObjId, 'comment_added', function(oData) { + $this.cmtUpdateCounterAs(oData); + $this.showLiveUpdateForSocket(oData); + }); + + // init post comment form + var sFormId = $this._sRootId + ' .cmt-post-reply form'; + if ($(sFormId).length) { + $(sFormId).each(function() { + $this.cmtInitFormPost($(this)); + }); + } + + // blink (highlight) necessary comments + $this._cmtsBlink($($this._sRootId)); + + // check content to show 'See More' + $this._cmtsInitSeeMore($($this._sRootId)); + }); +}; + +BxDolCmts.prototype.cmtInitFormPost = function(oCmtForm) +{ + var $this = this; + + oCmtForm.ajaxForm({ + dataType: "json", + beforeSubmit: function (formData, jqForm, options) { + window[$this._sObjName].cmtBeforePostSubmit(oCmtForm); + }, + success: function (oData) { + window[$this._sObjName].cmtAfterPostSubmit(oCmtForm, oData); + } + }); +}; + +BxDolCmts.prototype.cmtShowForm = function(oElement) +{ + var oForm = $(oElement).parents('.cmt-reply.cmt-reply-min:first'); + oForm.find('.cmt-body-min').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() { + oForm.find('.cmt-body').show(function() { + var oTextarea = oForm.find("[name='cmt_text']"); + if(oTextarea && oTextarea.hasClass('bx-form-input-html') && typeof bx_editor_get_htmleditable === 'function') + oTextarea = bx_editor_get_htmleditable(oTextarea); + + if(!oTextarea || oTextarea.length == 0) + return; + + if (oTextarea.attr('object_editor')) + eval('bx_editor_activate(' + oTextarea.attr('object_editor') + ')'); + else + oTextarea.focus(); + }); + }); +}; + +BxDolCmts.prototype.cmtBeforePostSubmit = function(oCmtForm) +{ + this._loadingInButton($(oCmtForm).children().find(':submit'), true); +}; + +BxDolCmts.prototype.cmtAfterPostSubmit = function (oCmtForm, oData) +{ + var $this = this; + var fContinue = function() { + var oParent = oCmtForm.parents('.cmt-reply:first'); + + if(oData && oData.id != undefined) { + $this._getCmt(oCmtForm, oData.id); + + //--- Update form + var iCmtParentId = parseInt(oData.parent_id); + if(iCmtParentId == 0) + $this._getForm(undefined, {CmtParent: iCmtParentId}, function(sFormWrp) { + if(sFormWrp && sFormWrp.length > 0) + sFormWrp = $(sFormWrp).html(); + + oParent.hide().html(sFormWrp).bxProcessHtml().show(); + + $this.cmtInitFormPost(oParent.find('form')); + }); + else + oParent.remove(); + + //--- Update counter + $this.cmtUpdateCounter(oCmtForm, oData); + + return; + } + + if(oData && oData.form != undefined && oData.form_id != undefined) { + oParent.find('form').replaceWith(oData.form); + + $this.cmtInitFormPost(oParent.find('form')); + + return; + } + }; + + this._loadingInButton($(oCmtForm).children().find(':submit'), false); + + if(oData && oData.msg != undefined) + bx_alert(oData.msg, fContinue); + else + fContinue(); +}; + +BxDolCmts.prototype.cmtInitFormEdit = function(sCmtFormId) +{ + var $this = this; + var oCmtForm = $('#' + sCmtFormId); + + oCmtForm.ajaxForm({ + dataType: "json", + beforeSubmit: function (formData, jqForm, options) { + window[$this._sObjName].cmtBeforeEditSubmit(oCmtForm); + }, + success: function (oData) { + window[$this._sObjName].cmtAfterEditSubmit(oCmtForm, oData); + } + }); +}; + +BxDolCmts.prototype.cmtBeforeEditSubmit = function(oCmtForm) +{ + this._loadingInButton($(oCmtForm).children().find(':submit'), true); +}; + +BxDolCmts.prototype.cmtAfterEditSubmit = function (oCmtForm, oData, onComplete) +{ + var $this = this; + var fContinue = function() { + if(oData && oData.id != undefined && oData.content != undefined) { + var iCmtId = parseInt(oData.id); + if(iCmtId > 0) { + $('#cmt' + iCmtId + ' .cmt-cont-cnt:first').bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $(this).html(oData.content).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed); + }); + } + } + + if(oData && oData.form != undefined && oData.form_id != undefined) { + $('#' + oData.form_id).replaceWith(oData.form); + $this.cmtInitFormEdit(oData.form_id); + } + + if(typeof onComplete == 'function') + onComplete(oCmtForm, oData); + }; + + this._loadingInButton($(oCmtForm).children().find(':submit'), false); + + if(oData && oData.msg != undefined) + bx_alert(oData.msg, fContinue); + else + fContinue(); +}; + +BxDolCmts.prototype.cmtUpdateCounter = function (oElement, oData) +{ + if(oData && (oData.count == undefined || parseInt(oData.count) == 0)) + return; + + if(oData.countf != undefined && oData.countf.length != 0) { + var oCounter = this._getCounter(oElement); + if(oCounter && oCounter.length > 0) + oCounter.replaceWith(oData.countf); + } + + var oCounter = this._getCounter(oElement, true); + if(oCounter && oCounter.length > 0) + oCounter.html(oData.count); + + var sClassHidden = 'sys-ac-hidden'; + if(!oCounter.is('.' + sClassHidden)) + oCounter = oCounter.parents('.' + sClassHidden + ':first'); + + oCounter.toggleClass(sClassHidden); +}; + +BxDolCmts.prototype.cmtUpdateCounterAs = function (oData) +{ + this.cmtUpdateCounter(null, JSON.parse(oData)); +}; + +BxDolCmts.prototype.cmtPin = function(oLink, iCmtId, iWay, bHideMenu) { + var $this = this; + + if(bHideMenu == undefined || bHideMenu) + $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide(); + + var oParams = this._getDefaultActions(); + oParams['action'] = 'Pin'; + oParams['Cmt'] = iCmtId; + oParams['way'] = iWay; + + var oCmt = $(this._sRootId + ' #cmt' + iCmtId); + this._loadingInBlock(oCmt, true); + + jQuery.post ( + this._sActionsUrl, + oParams, + function (oData) { + var fContinue = function() { + if(oData && oData.parent_id != undefined) { + $this._getCmts(oLink, { + CmtParent: oData.parent_id, + CmtBrowse: $this._sBrowseType, + CmtFilter: $this._sBrowseFilter, + CmtDisplay: $this._sDisplayType, + CmtPinned: 1 + }, function(sListId, sContent) { + $this._loadingInBlock(oCmt, false); + + $this._sDisplayType = $this._sDisplayType; + $this._cmtsReplaceContent($(sListId), sContent); + + var oDivider = $(sListId).siblings('.cmts-divider'); + if(oDivider.length > 0) { + if(oDivider.is(':hidden') && sContent.length != 0) + oDivider.show(); + else if(!oDivider.is(':hidden') && sContent.length == 0) + oDivider.hide(); + } + }); + } + }; + + if(oData && oData.msg != undefined) + bx_alert(oData.msg, fContinue); + else + fContinue(); + }, + 'json' + ); +}; + +BxDolCmts.prototype.cmtEdit = function(oLink, iCmtId, bHideMenu) { + var $this = this; + + if(bHideMenu == undefined || bHideMenu) + $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide(); + + var sContentId = this._sRootId + ' #cmt' + iCmtId + ' .cmt-cont-cnt:first'; + if ($(sContentId + ' > form').length) { + $(sContentId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $(this).html($this._oSavedTexts[iCmtId]).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed); + }); + + return; + } + + var oParams = this._getDefaultActions(); + oParams['action'] = 'GetFormEdit'; + oParams['Cmt'] = iCmtId; + + this._oSavedTexts[iCmtId] = $(sContentId).html(); + + this._loadingInContent(oLink, true); + + jQuery.post ( + this._sActionsUrl, + oParams, + function (oData) { + var fContinue = function() { + if(oData && oData.form != undefined && oData.form_id != undefined) { + $(sContentId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $(this).html(oData.form).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $this.cmtInitFormEdit(oData.form_id); + }); + }); + } + }; + + $this._loadingInContent(oLink, false); + + if(oData && oData.msg != undefined) + bx_alert(oData.msg, fContinue); + else + fContinue(); + }, + 'json' + ); +}; + +BxDolCmts.prototype.cmtRemove = function(e, iCmtId) { + var $this = this; + + $(e).parents('.bx-popup-active:first').dolPopupHide(); + + bx_confirm('', function() { + var oParams = $this._getDefaultActions(); + oParams['action'] = 'Remove'; + oParams['Cmt'] = iCmtId; + + $this._loadingInContent(e, true); + + jQuery.post ( + $this._sActionsUrl, + oParams, + function(oData) { + var fContinue = function() { + if(oData && oData.id != undefined) { + $(e).parents('.bx-popup-applied:first:visible').dolPopupHide(); + + var fDelete = function() { + var sCmtSel = '#cmt' + oData.id; + + $(sCmtSel).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + //--- Update counter + if(oData && oData.count != undefined && parseInt(oData.count) >= 0) { + var oSource = $(this); + + var oCounter = $this._getCounter(oSource); + if(oCounter && oCounter.length > 0) { + if(oData.countf != undefined && oData.countf.length != 0) + oCounter.html(oData.countf); + + if(parseInt(oData.count) == 0) + oCounter.toggleClass('sys-ac-hidden'); + } + + var oCounter = $this._getCounter(oSource, true); + if(oCounter && oCounter.length > 0) + oCounter.html(oData.count); + } + + $(this).remove(); + + var oCmt = $(sCmtSel); + if(oCmt && oCmt.length > 0) + fDelete(); + }); + }; + + fDelete(); + } + }; + + $this._loadingInContent(e, false); + + if(oData && oData.msg != undefined) + bx_alert(oData.msg, fContinue); + else + fContinue(); + }, + 'json' + ); + }); +}; + +BxDolCmts.prototype.cmtLoad = function(oLink, iCmtParentId, iStart, iPerView) +{ + var $this = this; + var bButton = $(oLink).hasClass('bx-btn'); + + if(bButton) + this._loadingInButton(oLink, true); + else + this._loading($(oLink).parents('ul.cmts:first'), true); + + this._getCmts(oLink, { + CmtParent: iCmtParentId, + CmtStart: iStart, + CmtPerView: iPerView, + CmtBrowse: this._sBrowseType, + CmtFilter: this._sBrowseFilter, + CmtDisplay: this._sDisplayType, + CmtDisplayStructure: this._iDisplayStructure + }, function(sListId, sContent) { + if(bButton) + $this._loadingInButton(oLink, false); + else + $this._loading($(oLink).parents('ul.cmts:first'), false); + + $this._cmtsReplace($(oLink).parents('li:first'), sContent); + }); +}; + +BxDolCmts.prototype.cmtChangeDisplay = function(oLink, sType) +{ + var $this = this; + this._getCmts(oLink, { + CmtParent: 0, + CmtBrowse: this._sBrowseType, + CmtFilter: this._sBrowseFilter, + CmtDisplay: sType + }, function(sListId, sContent) { + $this._sDisplayType = sType; + $this._cmtsReplaceContent($(sListId), sContent); + }); +}; + +BxDolCmts.prototype.cmtChangeBrowse = function(oLink, sType) +{ + var $this = this; + this._getCmts(oLink, { + CmtParent: 0, + CmtBrowse: sType, + CmtFilter: this._sBrowseFilter, + CmtDisplay: this._sDisplayType + }, function(sListId, sContent) { + $this._sBrowseType = sType; + $this._cmtsReplaceContent($(sListId), sContent); + }); +}; + +BxDolCmts.prototype.cmtChangeFilter = function(oLink, sType) +{ + var $this = this; + this._getCmts(oLink, { + CmtParent: 0, + CmtBrowse: this._sBrowseType, + CmtFilter: sType, + CmtDisplay: this._sDisplayType + }, function(sListId, sContent) { + $this._sBrowseFilter = sType; + $this._cmtsReplaceContent($(sListId), sContent); + }); +}; + +BxDolCmts.prototype.showReplacement = function(iCmtId) +{ + $('#cmt' + iCmtId + '-hidden').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function(){ + $(this).next('#cmt' + iCmtId).bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed); + }); +}; + +BxDolCmts.prototype.showImage = function(eve, oLink) +{ + var $this = this; + var ePswp = document.querySelectorAll('.pswp.cmts')[0]; + + var aItems = []; // more items are added dynamically + var oItem = {}; + var iCount = 0; + var options = { + shareEl: false, + counterEl: false, + history: false, + loop: false, + showHideOpacity: true, + index: 0, + getThumbBoundsFn: function(index) { + var e = $('a[data-file-id] img'); + if (!e.length) + return false; + + return {x:e.offset().left, y:e.offset().top, w:e.width()}; + } + }; + + var fnProcessRetina = function (o) { + var dpr = ((window.glBxDisableRetina !== undefined && window.glBxDisableRetina) || window.devicePixelRatio === undefined ? 1 : window.devicePixelRatio); + if (dpr < 2) + return o; + o.w *= 2; + o.h *= 2; + return o; + }; + + eve.preventDefault ? eve.preventDefault() : eve.returnValue = false; + + // get data for initial item from the attributes of the item which was clicked + $.each(oLink.attributes, function(i, attr) { + var sName = attr.name; + if (sName.indexOf('data-') !== 0) + return; + + sName = sName.replace('data-', ''); + oItem[sName] = attr.value; + + ++iCount; + }); + + oItem.msrc = oItem.src; + oItem = fnProcessRetina(oItem); + + if(!iCount) + return false; + + aItems.push(oItem); + + var fnConverMedia = function (oMedia) { + var oMap = { + 'id': 'file-id', + 'file' : 'src', + 'w': 'w', + 'h': 'h' + }; + + var o = {}; + for (var i in oMap) + o[oMap[i]] = oMedia[i]; + + return fnProcessRetina(o); + }; + + var fnIndexOfMediaObject = function (oArray, oItem) { + var iLength = oArray.length; + for (var i=0 ; i < iLength ; ++i) + if (oItem['file-id'] == oArray[i]['file-id']) + return i; + + return -1; + }; + + var fnDisableArrows = function (oItem) { + + // disable prev item and action if we are on the first item + if (0 == fnIndexOfMediaObject(aItems, oItem)) { + $('.pswp.cmts .pswp__button--arrow--left').hide(); + glSysCmtsPrevFn = glSysCmtsGallery.prev; + glSysCmtsGallery.prev = function () { }; + } + else { + $('.pswp.cmts .pswp__button--arrow--left').show(); + if ('undefined' !== typeof(glSysCmtsPrevFn)) + glSysCmtsGallery.prev = glSysCmtsPrevFn; + } + + // disable next item and action when we are in the last item + if ((aItems.length-1) == fnIndexOfMediaObject(aItems, oItem)) { + $('.pswp.cmts .pswp__button--arrow--right').hide(); + glSysCmtsNextFn = glSysCmtsGallery.next; + glSysCmtsGallery.next = function () { }; + } + else { + $('.pswp.cmts .pswp__button--arrow--right').show(); + if ('undefined' !== typeof(glSysCmtsNextFn)) + glSysCmtsGallery.next = glSysCmtsNextFn; + } + }; + + var fnLoadMoreItem = function (oItemCurrent, bFirstLoad) { + + // load addutional items only for items on the border + if (0 != fnIndexOfMediaObject(aItems, oItemCurrent) && (aItems.length-1) != fnIndexOfMediaObject(aItems, oItemCurrent)) + return; + + var sUrl = bx_append_url_params($this._sActionsUrl, {sys: $this._sSystem, id: $this._iObjId, action: 'GetSiblingFiles', file_id: oItemCurrent['file-id']}); + $.getJSON(sUrl, function(oData) { + + if ('undefined' !== typeof(oData.error)) { + if ('undefined' !== typeof(console)) + console.log(oData.error); + + return; + } + + var iGoTo = -1; + var iLength = aItems.length; + + if (0 == fnIndexOfMediaObject(aItems, oItemCurrent) && 'undefined' !== typeof(oData.prev.file)) { + aItems.unshift(fnConverMedia(oData.prev)); + if (bFirstLoad) + options.index = 1; + else + iGoTo = glSysCmtsGallery.getCurrentIndex() + 1; + } + + if ((aItems.length - 1) == fnIndexOfMediaObject(aItems, oItemCurrent) && 'undefined' !== typeof(oData.next.file)) + aItems.push(fnConverMedia(oData.next)); + + if (!bFirstLoad && iLength != aItems.length) { + glSysCmtsGallery.invalidateCurrItems(); + glSysCmtsGallery.updateSize(true); + + if(iGoTo >= 0) + glSysCmtsGallery.goTo(iGoTo); + } + + if (bFirstLoad) { + glSysCmtsGallery = new PhotoSwipe(ePswp, PhotoSwipeUI_Default, aItems, options); + glSysCmtsGallery.init(); + + glSysCmtsGallery.listen('beforeChange', function() { + // load more items if we are on first or on the last item + fnLoadMoreItem(glSysCmtsGallery.currItem, false); + }); + + glSysCmtsGallery.listen('afterChange', function() { + fnDisableArrows(glSysCmtsGallery.currItem); + }); + + glSysCmtsGallery.listen('close', function() { + }); + + fnDisableArrows(glSysCmtsGallery.currItem); + } + }); + }; + + fnLoadMoreItem(oItem, true); + + return false; +}; + +BxDolCmts.prototype.toggleReply = function(oElement, iCmtParentId, iQuote) +{ + var $this = this; + var aParams = { + CmtParent: iCmtParentId, + CmtQuote: iQuote != undefined ? parseInt(iQuote) : 0 + }; + var fOnShow = function() { + $(this).find('textarea:first').focus(); + }; + + var oParentId = null; + if(oElement) + oParentId = $(oElement).parents('#cmt' + iCmtParentId); + else + oParentId = $(this._sRootId + ' #cmt' + iCmtParentId); + + var sReplyQuoteId = '.cmt-reply-quote'; + var sReplyQuoteIdOpst = ':not(' + sReplyQuoteId + ')'; + + var sReplyId = ''; + var sReplyIdOpst = ''; + if(aParams['CmtQuote']) { + sReplyId = '.cmt-reply' + sReplyQuoteId; + sReplyIdOpst = '.cmt-reply' + sReplyQuoteIdOpst; + } + else { + sReplyId = '.cmt-reply' + sReplyQuoteIdOpst; + sReplyIdOpst = '.cmt-reply' + sReplyQuoteId; + } + + //--- Hide opposite form. + if (oParentId.find('> ' + sReplyIdOpst + ':visible').length) + oParentId.find('> ' + sReplyIdOpst).hide(); + + if (oParentId.find('> ' + sReplyId).length) { + oParentId.find('> ' + sReplyId).bx_anim('toggle', this._sAnimationEffect, this._iAnimationSpeed, fOnShow); + return; + } + + this._getForm(oElement, aParams, function(sForm) { + var oForm = $(sForm).hide().addClass('cmt-reply-' + $this._sPostFormPosition).addClass('cmt-reply-margin'); + var oFormSibling = oParentId.find('> ul.cmts:first'); + switch($this._sPostFormPosition) { + case 'top': + oFormSibling.before(oForm); + break; + + case 'bottom': + oFormSibling.after(oForm); + break; + } + + $this.cmtInitFormPost(oForm.find('form')); + + oParentId.children(sReplyId).bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed, fOnShow); + }); +}; + +BxDolCmts.prototype.toggleQuote = function(e, iCmtParentId) +{ + this.toggleReply(e, iCmtParentId, 1); +}; + +BxDolCmts.prototype.goTo = function(oLink, sGoToId, sBlinkIds, onLoad) +{ + $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide(); + + var $this = this; + this._getCmts(null, { + CmtParent: 0, + CmtBrowse: this._sBrowseType, + CmtFilter: this._sBrowseFilter, + CmtDisplay: this._sDisplayType, + CmtBlink: sBlinkIds + }, function(sListId, sContent) { + $this._cmtsReplaceContent($(sListId), sContent, function() { + location.hash = sGoToId; + + if(typeof onLoad == 'function') + onLoad(); + }); + }); +}; + +BxDolCmts.prototype.goToBtn = function(oLink, sGoToId, sBlinkIds, onLoad) +{ + var $this = this; + + this._loadingInButton(oLink, true); + + this._getCmts(null, { + CmtParent: 0, + CmtBrowse: this._sBrowseType, + CmtFilter: this._sBrowseFilter, + CmtDisplay: this._sDisplayType, + CmtBlink: sBlinkIds + }, function(sListId, sContent) { + $this._cmtsReplaceContent($(sListId), sContent, function() { + location.hash = sGoToId; + + if(typeof onLoad == 'function') + onLoad(); + + $(oLink).parents('.cmt-lu-button:first').remove(); + + $this.resumeLiveUpdates(); + }); + }); +}; + +BxDolCmts.prototype.goToAndReply = function(oLink, sGoToId, sBlinkIds) +{ + var $this = this; + this.goTo(oLink, sGoToId, sBlinkIds, function() { + var aBlinkIds = sBlinkIds.split(","); + $.each(aBlinkIds, function(iIndex, iValue) { + $this.toggleReply(null, iValue); + }); + }); +}; + +/*----------------------------*/ +/*--- Live Updates methods ---*/ +/*----------------------------*/ +BxDolCmts.prototype.showLiveUpdate = function(oData) +{ + if(!oData.code) + return; + + var oButton = $(oData.code); + var sId = oButton.attr('id'); + $('#' + sId).remove(); + + oButton.prependTo(this._sRootId); +}; + +BxDolCmts.prototype.showLiveUpdateForSocket = function(sData) +{ + var oData = JSON.parse(sData); + if(this._iAuthorId == oData.author_id) + return; + + var sSelector = this._sRootId + ' .' + this._sSP + '-lu-button'; + if($(sSelector + ':visible').length != 0) + return; + + var oElement = $(sSelector + ':hidden').clone(); + if(!oElement || oElement.length == 0) + return; + + var sOnClick = oElement.find('.bx-btn').attr('onclick'); + oElement.find('.bx-btn').attr('onclick', sOnClick.replace('{cmt_id}', oData.id).replace('{cmt_anchor}', oData.anchor)); + oElement.prependTo(this._sRootId).show(); +}; + +BxDolCmts.prototype.showLiveUpdates = function(oData) +{ + /* + * Note. oData.count_old and oData.count_new are also available and can be checked or used in notification popup. + */ + if(!oData.code) + return; + + var $this = this; + + var oNotifs = $(oData.code); + var sId = oNotifs.attr('id'); + $('#' + sId).remove(); + + oNotifs.prependTo('body').dolPopup({ + position: 'fixed', + left: '1rem', + top: 'auto', + bottom: '1rem', + fog: false, + onBeforeShow: function() { + }, + onBeforeHide: function() { + }, + onShow: function() { + setTimeout(function() { + $('.bx-popup-chain.bx-popup-applied:visible:first').dolPopupHide(); + }, 5000); + }, + onHide: function() { + $this.resumeLiveUpdates(); + } + }); +}; + +BxDolCmts.prototype.previousLiveUpdate = function(oLink) +{ + var fPrevious = function() { + var sClass = 'bx-popup-chain-item'; + $(oLink).parents('.' + sClass + ':first').hide().prev('.' + sClass).show(); + }; + + if(!this.pauseLiveUpdates(fPrevious)); + fPrevious(); +}; + +BxDolCmts.prototype.hideLiveUpdate = function(oLink) +{ + $(oLink).parents('.bx-popup-applied:visible:first').dolPopupHide(); +}; + +BxDolCmts.prototype.resumeLiveUpdates = function(onLoad) +{ + if(!this._bLiveUpdatePaused) + return false; + + var $this = this; + this.changeLiveUpdates('ResumeLiveUpdate', function() { + $this._bLiveUpdatePaused = false; + + if(typeof onLoad == 'function') + onLoad(); + }); + + return true; +}; + +BxDolCmts.prototype.pauseLiveUpdates = function(onLoad) +{ + if(this._bLiveUpdatePaused) + return false; + + var $this = this; + this.changeLiveUpdates('PauseLiveUpdate', function() { + $this._bLiveUpdatePaused = true; + + if(typeof onLoad == 'function') + onLoad(); + }); + + return true; +}; + +BxDolCmts.prototype.changeLiveUpdates = function(sAction, onLoad) +{ + var $this = this; + var oParams = this._getDefaultActions(); + oParams['action'] = sAction; + + jQuery.get( + this._sActionsUrl, + oParams, + function() { + if(typeof onLoad == 'function') + onLoad(); + } + ); +}; + +BxDolCmts.prototype.cmtShowMore = function(oLink) +{ + var oShowMore = $(oLink).parents('.' + this._sSP + '-body-show-more:first'); + if(!oShowMore.length) + return; + + var sClassOverflow = 'bx-overflow'; + + oShowMore.siblings('.' + sClassOverflow).css('max-height', 'none').removeClass(sClassOverflow); + oShowMore.remove(); +}; + +BxDolCmts.prototype.cmtOnFindOverflow = function(oElement) { + $(oElement).after($(oElement).parents(this._sRootId + ':first').find('.' + this._sSP + '-body-show-more:hidden:first').clone().show()); +}; + +/*----------------------------------*/ +/*--- Methods for internal usage ---*/ +/*----------------------------------*/ +BxDolCmts.prototype._getCmt = function (e, iCmtId) +{ + var $this = this; + var oData = this._getDefaultActions(); + oData['action'] = 'GetCmt'; + oData['Cmt'] = iCmtId; + oData['CmtBrowse'] = this._sBrowseType; + oData['CmtDisplay'] = this._sDisplayType; + + this._loadingInBlock (e); + + jQuery.post ( + this._sActionsUrl, + oData, + function (oData) { + $this._loadingInBlock (e, false); + + var sListId = $this._sRootId + ' #cmt' + oData.vparent_id + ' > ul.cmts-all'; + var sReplyFormId = $this._sRootId + ' #cmt' + oData.parent_id + ' > .cmt-reply'; + + //--- Hide reply form ---// + if($(sReplyFormId).length) + $(sReplyFormId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed); + + $(sListId).each(function() { + //--- Some number of comments already loaded ---// + if($(this).children('li.cmt').length) { + var oAdded = null; + if($('#cmt' + iCmtId).length) { + $('#cmt' + iCmtId).replaceWith($(oData.content).hide()); + + oAdded = $('#cmt' + iCmtId); + } + else { + switch($this._sPostFormPosition) { + case 'top': + oAdded = $(this).children('li.cmt:first').before($(oData.content).hide()).prevAll('li.cmt:hidden'); + break; + + case 'bottom': + oAdded = $(this).children('li.cmt:last').after($(oData.content).hide()).nextAll('li.cmt:hidden'); + break; + } + } + + oAdded.each(function() { + $(this).bxProcessHtml().bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed); + }); + } + //-- There is no comments at all ---// + else + $(this).hide().html(oData.content).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed); + + $this._cmtsInitSeeMore($(this)); + }); + }, + 'json' + ); +}; + +BxDolCmts.prototype._getCmts = function (oElement, oRequestParams, onLoad) +{ + var $this = this; + var oData = this._getDefaultActions(); + oData['action'] = 'GetCmts'; + oData = $.extend({}, oData, oRequestParams); + + var sListId = this._sRootId + ' #cmt' + oData['CmtParent'] + ' > ul.cmts-' + (oRequestParams.CmtPinned ? 'pinned' : 'all') + ':first'; + + if(oElement) + this._loadingInBlock(oElement, true); + + jQuery.post ( + this._sActionsUrl, + oData, + function(s) { + if(oElement) + $this._loadingInBlock(oElement, false); + + if(typeof onLoad == 'function') + onLoad(sListId, s); + else + $this._cmtsAppend(sListId, s); + } + ); +}; + +BxDolCmts.prototype._getForm = function (e, oParams, onLoad) +{ + var $this = this; + var oData = $.extend({}, this._getDefaultActions(), { + action: 'GetFormPost', + CmtType: 'reply', + CmtBrowse: this._sBrowseType, + CmtDisplay: this._sDisplayType, + CmtMinPostForm: this._iMinPostForm + }, oParams); + + if(e) + this._loadingInContent(e, true); + + jQuery.post ( + this._sActionsUrl, + oData, + function (s) { + if(e) + $this._loadingInContent(e, false); + + if(typeof onLoad == 'function') + onLoad(s); + } + ); +}; + +BxDolCmts.prototype._cmtsAppend = function(sIdTo, sContent) +{ + $(sIdTo).append($(sContent).hide()).children(':hidden').bxProcessHtml().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed); +}; + +BxDolCmts.prototype._cmtsPrepend = function(sIdTo, sContent) +{ + $(sIdTo).prepend($(sContent).hide()).children(':hidden').bxProcessHtml().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed); +}; + +BxDolCmts.prototype._cmtsReplace = function(oReplace, sContent, onLoad) +{ + var $this = this; + $(oReplace).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() { + $(this).after($(sContent).hide()).nextAll(':hidden').bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $this._cmtsBlink($(this)); + + $this._cmtsInitSeeMore($(this)); + + if(typeof onLoad == 'function') + onLoad(); + }); + $(this).remove(); + }); +}; +BxDolCmts.prototype._cmtsReplaceContent = function(oParent, sContent, onLoad) +{ + var $this = this; + $(oParent).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() { + $(this).html(sContent).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() { + $this._cmtsBlink($(this)); + + $this._cmtsInitSeeMore($(this)); + + if(typeof onLoad == 'function') + onLoad(); + }); + }); +}; + +BxDolCmts.prototype._cmtsBlink = function(oParent) +{ + var sBlinkClass = 'cmt-blink'; + + oParent.find('.' + sBlinkClass + '-plate:visible').animate({ + opacity: 0 + }, 5000, function() { + oParent.find('.' + sBlinkClass).removeClass(sBlinkClass); + }); +}; + +BxDolCmts.prototype._cmtsInitSeeMore = function(oParent) +{ + var $this = this; + + oParent.find('.cmt-body.bx-overflow-ready').bxCheckOverflowHeight('', function(oElement) { + $this.cmtOnFindOverflow(oElement); + }); +}; + + + +BxDolCmts.prototype._getCounter = function(oElement, bText) +{ + var oCounter = null; + var sSelector = '.' + this._sSP + '-counter' + (bText ? '-text' : ''); + + if(oElement) { + if($(oElement).hasClass(this._sSP)) + oCounter = $(oElement).find(sSelector); + else + oCounter = $(oElement).parents('.' + this._sSP + ':first').find(sSelector); + } + + if((!oCounter || !oCounter.length) && this._aHtmlIds['counter'] != undefined) { + oCounter = $('#' + this._aHtmlIds['counter']); + if(!oCounter.is(sSelector)) + oCounter = oCounter.find(sSelector); + } + + return oCounter; +}; + +BxDolCmts.prototype._getDefaultActions = function() { + var oDate = new Date(); + var oResult = { + sys: this._sSystem, + id: this._iObjId, + _t: oDate.getTime() + }; + if(this._aParams) + oResult['params'] = this._aParams; + + return oResult; +}; + +BxDolCmts.prototype._loading = function(e, bShow) { + var oParent = $(e).length ? $(e) : $('body'); + bx_loading(oParent, bShow); +}; + +BxDolCmts.prototype._loadingInBlock = function(e, bShow) { + var oParent = $(e).length ? $(e).parents('.bx-db-content:first') : $('body'); + bx_loading(oParent, bShow); +}; + +BxDolCmts.prototype._loadingInContent = function(e, bShow) { + var oParent = $(e).length ? $(e).parents('li.cmt:first,.cmt-reply:first') : $('body'); + bx_loading(oParent, bShow); +}; + +BxDolCmts.prototype._loadingInButton = function(e, bShow) { + if($(e).length) + bx_loading_btn($(e), bShow); + else + bx_loading($('body'), bShow); +}; + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolScore.js b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolScore.js new file mode 100644 index 0000000000..76092be571 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolScore.js @@ -0,0 +1,165 @@ +/** + * Copyright (c) UNA, Inc - https://una.io + * MIT License - https://opensource.org/licenses/MIT + * + * @defgroup UnaCore UNA Core + * @{ + */ + +function BxDolScore(oOptions) +{ + this._sObjName = undefined == oOptions.sObjName ? 'oScore' : oOptions.sObjName; // javascript object name, to run current object instance from onTimer + this._sSystem = oOptions.sSystem; // current score system + this._iAuthorId = oOptions.iAuthorId; // score's author ID. + this._iObjId = oOptions.iObjId; // object id the scores are collected for + + this._sActionsUri = 'score.php'; + this._sActionsUrl = oOptions.sRootUrl + this._sActionsUri; // actions url address + this._sSocket = oOptions.sSocket === undefined ? this._sSystem : oOptions.sSocket; + + this._sAnimationEffect = 'fade'; + this._iAnimationSpeed = 'slow'; + this._sSP = undefined == oOptions.sStylePrefix ? 'bx-score' : oOptions.sStylePrefix; + this._aHtmlIds = oOptions.aHtmlIds; + this._aRequestParams = oOptions.aRequestParams; + + this._iSaveWidth = -1; + + var $this = this; + $(document).ready(function() { + $this.init(); + }); +} + +BxDolScore.prototype.init = function() +{ + var $this = this; + $(document).ready(function() { + if(window.oBxDolSockets !== undefined && $this._sSocket) + oBxDolSockets.subscribe($this._sSocket, $this._iObjId, 'voted', function(oData) { + $this.onVoteAs(oData); + }); + }); + + return; +}; + +BxDolScore.prototype.toggleByPopup = function(oLink, sType) { + var oParams = this._getDefaultParams(); + oParams['action'] = 'GetVotedBy'; + if(sType) + oParams['type'] = sType; + + $(oLink).dolPopupAjax({ + id: this._aHtmlIds['by_popup'], + url: bx_append_url_params(this._sActionsUri, oParams), + removeOnClose: true + }); +}; + +BxDolScore.prototype.voteUp = function (oLink, onComplete) +{ + this._vote(oLink, 'up', onComplete); +}; + +BxDolScore.prototype.voteDown = function (oLink, onComplete) +{ + this._vote(oLink, 'down', onComplete); +}; + +BxDolScore.prototype.onVoteAs = function(sData) +{ + oData = JSON.parse(sData); + + var oLink = $('#' + this._aHtmlIds['main']).find('.' + this._sSP + '-do-vote.' + this._sSP + '-dv-' + oData.type); + if(oLink && oLink.length) + this._onVote(oLink, oData); +}; + +BxDolScore.prototype._vote = function (oLink, sType, onComplete) +{ + var $this = this; + var oParams = this._getDefaultParams(); + oParams['action'] = 'vote_' + sType; + + $.post( + this._sActionsUrl, + oParams, + function(oData) { + if(oData && oData.message != undefined) + bx_alert(oData.message, function() { + $this._onVote(oLink, oData, onComplete); + }); + else + $this._onVote(oLink, oData, onComplete); + }, + 'json' + ); +}; + +BxDolScore.prototype._onVote = function(oLink, oData, onComplete) +{ + if(oData && oData.code != 0) + return; + + if(oData && oData.label_icon) + $(oLink).find('.sys-action-do-icon .sys-icon').attr('class', 'sys-icon ' + oData.label_icon); + + if(oData && oData.label_title) { + $(oLink).attr('title', oData.label_title); + $(oLink).find('.sys-action-do-text').html(oData.label_title); + } + + if(oData && oData.disabled) + this._getActions(oLink).removeAttr('onclick').addClass($(oLink).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-score-disabled'); + + var oCounter = this._getCounter(oLink); + if(oCounter && oCounter.length > 0) { + if(oData && oData.counter) + oCounter.replaceWith(oData.counter); + else + oCounter.html(oData.scoref).parents('.' + $this._sSP + '-counter-holder:first:hidden').bx_anim('show'); + } + + if(typeof onComplete == 'function') + onComplete(oLink, oData); +}; + +BxDolScore.prototype._getActions = function(oElement) { + var oParent = $(oElement); + if(!oParent.hasClass(this._sSP)) + oParent = $(oElement).parents('.' + this._sSP + ':first'); + + return oParent.find('.' + this._sSP + '-do-vote'); +}; + +BxDolScore.prototype._getCounter = function(oElement) { + var oParent = $(oElement); + if(!oParent.hasClass(this._sSP)) + oParent = $(oElement).parents('.' + this._sSP + ':first'); + + var oCounter = oParent.find('.' + this._sSP + '-counter'); + if(oCounter && oCounter.length > 0) + return oCounter; + + return $('#' + this._aHtmlIds['counter'] + '.' + this._sSP + '-counter'); +}; + +BxDolScore.prototype._loadingInButton = function(e, bShow) { + if($(e).length) + bx_loading_btn($(e), bShow); + else + bx_loading($('body'), bShow); +}; + +BxDolScore.prototype._getDefaultParams = function() { + var oDate = new Date(); + return { + sys: this._sSystem, + id: this._iObjId, + params: $.param(this._aRequestParams), + _t: oDate.getTime() + }; +}; + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolUploader.js b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolUploader.js new file mode 100644 index 0000000000..918fe89015 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolUploader.js @@ -0,0 +1,1161 @@ +/** + * Copyright (c) UNA, Inc - https://una.io + * MIT License - https://opensource.org/licenses/MIT + * + * @defgroup UnaCore UNA Core + * @{ + */ + +/** + * Simple Uploader js class + */ + + +function BxDolUploaderBase (sUploaderObject, sStorageObject, sUniqId, options) { + + this.init(sUploaderObject, sStorageObject, sUniqId, options); + + this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe'; + this._eForm = null; +} + +BxDolUploaderBase.prototype.init = function (sUploaderObject, sStorageObject, sUniqId, options) { + this._isUploadsInProgress = false; + + this._sUploaderObject = sUploaderObject; + this._sStorageObject = sStorageObject; + + this._sUniqId = sUniqId; + + this._sUploaderJsInstance = 'glUploader_' + sUniqId + '_' + this._sUploaderObject; + this._sUploadInProgressContainerId = 'bx-form-input-files-' + sUniqId + '-upload-in-progress-' + this._sUploaderObject; + this._sPopupContainerId = 'bx-form-input-files-' + sUniqId + '-popup-wrapper-' + this._sUploaderObject; + + this._sResultContainerId = 'bx-form-input-files-' + sUniqId + '-upload-result'; + this._sErrorsContainerId = 'bx-form-input-files-' + sUniqId + '-errors'; + this._sProgressContainerId = 'bx-form-input-files-' + sUniqId + '-progress'; + + this._sFormContainerId = 'bx-form-input-files-' + sUniqId + '-form-cont'; + + this._sTemplateGhost = options.template_ghost ? options.template_ghost : '
{file_name} (delete)
'; + this._sTemplateReorder = options.template_reorder ? options.template_reorder : '
'; + this._sTemplateError = options.template_error_msg ? options.template_error_msg : '
{error}
' ; + this._sTemplateErrorGhosts = options.template_error_ghosts ? options.template_error_ghosts : this._sTemplateError; + + this._isMultiple = undefined == options.multiple || !options.multiple ? false : true; + this._isLatest = undefined == options.latest || !options.latest ? false : true; + this._isReordering = undefined == options.reordering || !options.reordering ? false : true; + + this._iContentId = undefined == options.content_id || '' == options.content_id ? '' : parseInt(options.content_id); + + this._sImagesTranscoder = options.images_transcoder ? options.images_transcoder : ''; + + this._isPrivate = undefined == options.storage_private || parseInt(options.storage_private) ? 1 : 0; + + this._isErrorShown = false; + + this._onUploadBefore = typeof options.on_upload_before === 'function' ? options.on_upload_before : function () {}; + this._onUpload = typeof options.on_upload === 'function' ? options.on_upload : function () {}; + this._onRestoreGhosts = typeof options.on_restore_ghosts === 'function' ? options.on_restore_ghosts : function () {}; +} + +BxDolUploaderBase.prototype.isMultiple = function () { + return this._isMultiple; +} + +BxDolUploaderBase.prototype.getCurrentFilesCount = function () { + return $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').length; +} + +BxDolUploaderBase.prototype.showUploaderForm = function () { + var $this = this; + var sUrl = this._getUrlWithStandardParams() + '&a=show_uploader_form&m=' + (this._isMultiple ? 1 : 0) + '&c=' + this._iContentId + '&p=' + this._isPrivate + '&_t=' + escape(new Date()); + + $(window).dolPopupAjax({ + url: sUrl, + id: {force: true, value: this._sPopupContainerId}, + onBeforeShow: function() { + if ($this.isMultiple()) + $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').show(); + else + $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').hide(); + $('#' + $this._sPopupContainerId + ' .bx-popup-element-close').click(function() { + $this.onClickCancel(); + }); + $("#" + $this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").hide(); + if ('undefined' !== typeof($this.onBeforeShowPopup)) + $this.onBeforeShowPopup(); + }, + onShow: function () { + if ('undefined' !== typeof($this.onShowPopup)) + $this.onShowPopup(); + }, + closeElement: false, + closeOnOuterClick: false + }); +} + +BxDolUploaderBase.prototype.onClickCancel = function () { + var $this = this; + if (this._isUploadsInProgress) { + bx_confirm(_t('_sys_uploader_confirm_close_popup'), function() { + $this.cancelAll(); + $('#' + $this._sPopupContainerId).dolPopupHide({}); + }); + } else { + $('#' + this._sPopupContainerId).dolPopupHide(); + } +} + +BxDolUploaderBase.prototype.onBeforeUpload = function (params) { + + this._eForm = params; + + this._loading(true, false); + + this._isUploadsInProgress = true; + this._lockPageFromLeaving(); + this._clearErrors(); + + if(typeof this._onUploadBefore === 'function') + this._onUploadBefore(this); +} + +BxDolUploaderBase.prototype.onProgress = function (params) { + +} + +BxDolUploaderBase.prototype.onUploadCompleted = function (sErrorMsg) { + + this._isUploadsInProgress = false; + this._unlockPageFromLeaving(); + this._loading(false, true); + + if (sErrorMsg.length) { + this.restoreGhosts(); + this._showError(sErrorMsg); + } else { + this.restoreGhosts(); + $('#' + this._sPopupContainerId).dolPopupHide({}); + + if(typeof this._onUpload === 'function') + this._onUpload(this, this._iContentId); + } +} + +BxDolUploaderBase.prototype.cancelAll = function () { + $('#' + this._sIframeId).attr('src', 'javascript:false;'); + this.onUploadCompleted(_t('_sys_uploader_upload_canceled')); +} + +BxDolUploaderBase.prototype.restoreGhosts = function (bInitReordering, onComplete) { + var sUrl = this._getUrlWithStandardParams() + '&img_trans=' + this._sImagesTranscoder + '&a=restore_ghosts&f=json' + '&c=' + this._iContentId + '&l=' + (this._isLatest ? 1 : 0) + '&_t=' + escape(new Date()); + var $this = this; + + bInitReordering = bInitReordering !== undefined ? bInitReordering : this._isReordering; + + $.getJSON(sUrl, function (aData) { + + if (!$this.isMultiple()) + $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').remove(); + + if ('object' === typeof(aData)) { + if('object' === typeof(aData.g) && 'object' === typeof(aData.o)) + for(var i in aData.o) { + var iFileId = aData.o[i]; + $this.showGhost(iFileId, aData.g[iFileId]); + } + else + $.each(aData, function(iFileId, oVars) { + $this.showGhost(iFileId, oVars); + }); + + $('#' + $this._sResultContainerId).bx_show_more_check_overflow(); + + if(bInitReordering) { + var sClassGhost = 'bx-uploader-ghost'; + $('#' + $this._sResultContainerId).find('.' + sClassGhost).each(function() { + if($(this).find('.bx-uploader-ghost-reorder').length == 0) + $(this).find('.bx-base-general-uploader-ghost').prepend($this._sTemplateReorder); + }); + + var fInitReordering = function() { + $('#' + $this._sResultContainerId).sortable({ + items: '.' + sClassGhost, + start: function(oEvent, oUi) { + oUi.item.addClass('bx-uploader-ghost-dragging'); + }, + stop: function(oEvent, oUi) { + oUi.item.removeClass('bx-uploader-ghost-dragging'); + + $this.reorderGhosts(oUi.item); + } + }); + }; + + if($.sortable !== undefined) + fInitReordering(); + else + setTimeout(fInitReordering, 2000); + } + } + + if(typeof onComplete === 'function') + onComplete(aData); + + if(typeof this._onRestoreGhosts === 'function') + this._onRestoreGhosts($this, aData); + }); +}; + +BxDolUploaderBase.prototype.reorderGhosts = function(oDraggable) { + var sUrl = this._getUrlWithStandardParams() + '&a=reorder_ghosts&f=json' + '&c=' + this._iContentId + '&' + $('#' + this._sResultContainerId).sortable('serialize', {key: 'ghosts[]'}) + '&_t=' + escape(new Date()); + + $.getJSON(sUrl, function (aData) { + processJsonData(aData); + }); +}; + +BxDolUploaderBase.prototype.showGhost = function(iId, oVars) { + var oFileContainer = $('#' + this._getFileContainerId(iId)); + if(oFileContainer.length > 0) + return; + + var sHTML; + if (typeof this._sTemplateGhost == 'object') + sHTML = this._sTemplateGhost[iId]; + else + sHTML = this._sTemplateGhost; + + for(var i in oVars) + sHTML = sHTML.replace (new RegExp('{' + i + '}', 'g'), oVars[i]); + + $('#' + this._sResultContainerId).append(sHTML).trigger( "contentchange" ); + + oFileContainer.find('.bx-uploader-ghost-preview img').hide().fadeIn(1000); +}; + +BxDolUploaderBase.prototype.deleteGhost = function (iFileId, onComplete) { + var sUrl = this._getUrlWithStandardParams() + '&a=delete&id=' + iFileId; + var $this = this; + + var sFileContainerId = $this._getFileContainerId(iFileId); + bx_loading(sFileContainerId, true); + + $.post(sUrl, {_t: escape(new Date())}, function (sMsg) { + var fOnComplete = function(sMsg) { + if(typeof onComplete === 'function') + onComplete(sMsg); + }; + + bx_loading(sFileContainerId, false); + + if ('ok' == sMsg) { + $('#' + sFileContainerId).slideUp('slow', function () { + $(this).remove(); + + fOnComplete(sMsg); + }); + } else { + $('#' + this._sResultContainerId).prepend($this._sTemplateErrorGhosts.replace('{error}', sMsg)); + + fOnComplete(sMsg); + } + }); +}; + +BxDolUploaderBase.prototype._clearErrors = function () { + $(' #' + this._sErrorsContainerId).html(''); + this._isErrorShown = false; +} + +BxDolUploaderBase.prototype._showError = function (s, bAppend) { + if (s == undefined || !s.length) + return; + + var eCont = $('#' + this._sPopupContainerId).find('[id=' + this._sErrorsContainerId + ']').size() ? $('#' + this._sPopupContainerId).find('[id=' + this._sErrorsContainerId + ']') : $('#' + this._sErrorsContainerId); + + if (!bAppend) + eCont.html(this._sTemplateError.replace('{error}', s)); + else + eCont.prepend(this._sTemplateError.replace('{error}', s)); + + this._isErrorShown = true; +}; + +BxDolUploaderBase.prototype._getFileContainerId = function (iFileId) { + return 'bx-uploader-file-' + this._sStorageObject + '-' + iFileId; +}; + +BxDolUploaderBase.prototype._getUrlWithStandardParams = function () { + return sUrlRoot + 'storage_uploader.php?uo=' + this._sUploaderObject + '&so=' + this._sStorageObject + '&uid=' + this._sUniqId; +} + +BxDolUploaderBase.prototype._lockPageFromLeaving = function () { + $(window).bind('beforeunload', function (e) { + var e = e || window.event; + // for ie, ff + e.returnValue = _t('_sys_uploader_confirm_leaving_page'); + // for webkit + return _t('_sys_uploader_confirm_leaving_page'); + }); +} + +BxDolUploaderBase.prototype._unlockPageFromLeaving = function () { + $(window).unbind('beforeunload'); +} + +BxDolUploaderBase.prototype._loading = function (bShowProgress, bShowForm) { + + var eForm = $('#' + this._sFormContainerId + ' .bx-uploader-files-list'); + var eBtn = $('#' + this._sFormContainerId + ' .bx-btn-primary'); + + if (bShowForm) { + if (null != this._eForm) { + eForm.find('.bx-uploader-simple-file').filter(':not(:first)').remove(); + if ('undefined' !== typeof(this._eForm.reset)) + this._eForm.reset(); + } + eForm.show(); + eBtn.show(); + } else { + eForm.hide(); + eBtn.hide(); + } + + bx_loading($('#' + this._sFormContainerId + ' .bx-uploader-loading').get(0), bShowProgress); +} + +BxDolUploaderBase.prototype.getMimeTypefromString = function (ext) { + + var mimeTypes = + { + + '3gp' : 'video/3gpp', + '3g2' : 'video/3gpp2', + 'a' : 'application/octet-stream', + 'ai' : 'application/postscript', + 'aif' : 'audio/x-aiff', + 'aifc' : 'audio/x-aiff', + 'aiff' : 'audio/x-aiff', + 'asf' : 'video/x-ms-asf', + 'au' : 'audio/basic', + 'avi' : 'video/x-msvideo', + 'avi' : 'video/avi', + 'bat' : 'text/plain', + 'bin' : 'application/octet-stream', + 'bmp' : 'image/x-ms-bmp', + 'c' : 'text/plain', + 'cdf' : 'application/x-cdf', + 'csh' : 'application/x-csh', + 'css' : 'text/css', + 'divx' : 'video/divx', + 'dll' : 'application/octet-stream', + 'doc' : 'application/msword', + 'dot' : 'application/msword', + 'drc' : 'video/drc', + 'dvi' : 'application/x-dvi', + 'eml' : 'message/rfc822', + 'eps' : 'application/postscript', + 'etx' : 'text/x-setext', + 'exe' : 'application/octet-stream', + 'gif' : 'image/gif', + 'gtar' : 'application/x-gtar', + 'h' : 'text/plain', + 'hdf' : 'application/x-hdf', + 'htm' : 'text/html', + 'html' : 'text/html', + 'jpe' : 'image/jpeg', + 'jpeg' : 'image/jpeg', + 'jpg' : 'image/jpeg', + 'js' : 'application/x-javascript', + 'ksh' : 'text/plain', + 'latex' : 'application/x-latex', + 'm1v' : 'video/mpeg', + 'man' : 'application/x-troff-man', + 'me' : 'application/x-troff-me', + 'mht' : 'message/rfc822', + 'mhtml' : 'message/rfc822', + 'mkv' : 'video/x-matroska', + 'mif' : 'application/x-mif', + 'mov' : 'video/quicktime', + 'movie' : 'video/x-sgi-movie', + 'mp2' : 'audio/mpeg', + 'mp3' : 'audio/mpeg', + 'mp4' : 'video/mp4', + 'm4v' : 'video/mp4', + 'mpa' : 'video/mpeg', + 'mpe' : 'video/mpeg', + 'mpeg' : 'video/mpeg', + 'mpg' : 'video/mpeg', + 'ms' : 'application/x-troff-ms', + 'nc' : 'application/x-netcdf', + 'nws' : 'message/rfc822', + 'o' : 'application/octet-stream', + 'obj' : 'application/octet-stream', + 'oda' : 'application/oda', + 'ogv' : 'video/ogg', + 'ogg' : 'video/ogg', + 'pbm' : 'image/x-portable-bitmap', + 'pdf' : 'application/pdf', + 'pfx' : 'application/x-pkcs12', + 'pgm' : 'image/x-portable-graymap', + 'png' : 'image/png', + 'pnm' : 'image/x-portable-anymap', + 'pot' : 'application/vnd.ms-powerpoint', + 'ppa' : 'application/vnd.ms-powerpoint', + 'ppm' : 'image/x-portable-pixmap', + 'pps' : 'application/vnd.ms-powerpoint', + 'ppt' : 'application/vnd.ms-powerpoint', + 'pptx' : 'application/vnd.ms-powerpoint', + 'ps' : 'application/postscript', + 'pwz' : 'application/vnd.ms-powerpoint', + 'py' : 'text/x-python', + 'pyc' : 'application/x-python-code', + 'pyo' : 'application/x-python-code', + 'qt' : 'video/quicktime', + 'ra' : 'audio/x-pn-realaudio', + 'ram' : 'application/x-pn-realaudio', + 'ras' : 'image/x-cmu-raster', + 'rdf' : 'application/xml', + 'rgb' : 'image/x-rgb', + 'rm' : 'application/vnd.rn-realmedia', + 'rmvb' : 'application/vnd.rn-realmedia-vbr', + 'roff' : 'application/x-troff', + 'rtx' : 'text/richtext', + 'sgm' : 'text/x-sgml', + 'sgml' : 'text/x-sgml', + 'sh' : 'application/x-sh', + 'shar' : 'application/x-shar', + 'snd' : 'audio/basic', + 'so' : 'application/octet-stream', + 'src' : 'application/x-wais-source', + 'swf' : 'application/x-shockwave-flash', + 't' : 'application/x-troff', + 'tar' : 'application/x-tar', + 'tcl' : 'application/x-tcl', + 'tex' : 'application/x-tex', + 'texi' : 'application/x-texinfo', + 'texinfo': 'application/x-texinfo', + 'tif' : 'image/tiff', + 'tiff' : 'image/tiff', + 'tr' : 'application/x-troff', + 'tsv' : 'text/tab-separated-values', + 'txt' : 'text/plain', + 'ustar' : 'application/x-ustar', + 'vcf' : 'text/x-vcard', + 'wav' : 'audio/x-wav', + 'wav' : 'audio/wav', + 'webm' : 'video/webm', + 'wmv' : 'video/wmv', + 'wiz' : 'application/msword', + 'wsdl' : 'application/xml', + 'xbm' : 'image/x-xbitmap', + 'xlb' : 'application/vnd.ms-excel', + 'xls' : 'application/vnd.ms-excel', + 'xlsx' : 'application/vnd.ms-excel', + 'xml' : 'text/xml', + 'xpdl' : 'application/xml', + 'xpm' : 'image/x-xpixmap', + 'xsl' : 'application/xml', + 'xvid' : 'video/xvid', + 'webp' : 'image/webp', + 'xwd' : 'image/x-xwindowdump', + 'svg' : 'image/svg+xml', + 'zip' : ['application/zip', 'application/x-zip-compressed'] + + } + return mimeTypes[ext.replace('.', '')]; +} + +/** + * HTML5 Uploader js class + */ +function BxDolUploaderHTML5 (sUploaderObject, sStorageObject, sUniqId, options) { + + this.init(sUploaderObject, sStorageObject, sUniqId, options); + + this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe'; + this._eForm = null; + this._sDivId = 'bx-form-input-files-' + sUniqId + '-div-' + this._sUploaderObject; + this._sFocusDivId = 'bx-form-input-files-' + sUniqId + '-focus-' + this._sUploaderObject; + this._uploader = null; + this._aFiles = []; + + + this.initUploader = function (o) { + + var $this = this; + + if (null != this._uploader) + this._uploader = null; + + var $this = this; + var _options = { + allowProcess: false, + allowPaste: false, + allowRevert: false, + allowRemove: false, + imagePreviewHeight: 100, + credits: {}, + allowMultiple: $this._isMultiple ? true : false, + maxFiles: $this.isMultiple() ? 50 : 1, + maxFileSize: o.maxFilesize > 2 ? Math.round(o.maxFilesize) + 'MB' : Math.round(o.maxFilesize*1024) + 'KB', + instantUpload: true, + onaddfile: (error, file) => { + if (error){ + $this._uploader.removeFile(file); + if($this._uploader.status != 3) + $this.onUploadCompleted(''); + } + else{ + $this.onBeforeUpload(''); + } + }, + onerror: (error) => { + if (error.main) + $this._showError(error.main + '. ' + error.sub); + }, + onprocessfiles: (files) => { + $this.onUploadCompleted(''); + oProgress.hide(); + $this._aFiles = []; + }, + onprocessfileprogress(file, progress) { + $this._aFiles[file.source.lastModified+'-'+file.source.size+'-'+file.source.name] = progress * file.source.size; + iTotal = 0; + iUploaded = 0; + for (const propertyName in $this._aFiles) { + iUploaded += $this._aFiles[propertyName]; + } + + for (i = 0; i < $this._uploader.getFiles().length; i++){ + iTotal += $this._uploader.getFiles()[i].source.size; + } + + iProgress = iUploaded / iTotal * 100; + + oProgress = $('#' + $this._sProgressContainerId); + + if (oProgress.parents('.bx-db-container').find('.uploader_progress').length > 0){ + oProgress = oProgress.parents('.bx-db-container').find('.uploader_progress'); + } + + oProgress.show(); + oProgress.find('.progress_line').css('width', iProgress + '%'); + }, + server: { + process: (fieldName, file, metadata, load, error, progress, abort) => { + const formData = new FormData(); + formData.append('file', file, file.name); + formData.append('uo', this._sUploaderObject); + formData.append('so', this._sStorageObject); + formData.append('uid', this._sUniqId); + formData.append('m', this._isMultiple ? 1 : 0); + formData.append('c', this._iContentId); + formData.append('p', this._isPrivate); + formData.append('a', 'upload'); + + const request = new XMLHttpRequest(); + request.open('POST', sUrlRoot + 'storage_uploader.php'); + + request.upload.onprogress = (e) => { + progress(e.lengthComputable, e.loaded, e.total); + }; + + request.onload = function(res) { + if (request.status >= 200 && request.status < 300) { + var o = null; + try { + o = JSON.parse(request.responseText); + } + catch (e) {} + if (o && 'undefined' !== typeof(o.error)) { + $this._showError(o.error, true); + } + + load(request.responseText); + } + else { + error('error'); + } + }; + request.send(formData); + } + }, + }; + + aAcceptableFiles = []; + if (o.acceptedFiles && o.acceptedFiles != ''){ + a = o.acceptedFiles.trim().split(/\s*,\s*/); + a.forEach(function(item, i, arr) { + if ($this.getMimeTypefromString(item)){ + oT = $this.getMimeTypefromString(item); + if (Array.isArray(oT)){ + oT.forEach(function(oTs, j, ar) { + aAcceptableFiles.push(oTs); + }); + } + else{ + aAcceptableFiles.push(oT); + } + } + + }); + } + + if (aAcceptableFiles.length){ + _options.acceptedFileTypes = aAcceptableFiles; + } + + + + if (o.resizeWidth || o.resizeHeight){ + _options.allowImageResize = true; + _options.imageResizeTargetWidth = o.resizeWidth; + _options.imageResizeTargetHeight = o.resizeHeight; + _options.imageResizeMode = o.resizeMethod; + } + else{ + _options.allowImageResize = false; + _options.allowImageTransform = false; + } + + FilePond.registerPlugin( + FilePondPluginImagePreview, + FilePondPluginFileValidateType, + FilePondPluginFileValidateSize, + FilePondPluginImageTransform, + FilePondPluginImageCrop, + FilePondPluginImageResize, + + ); + + this._uploader = FilePond.create( + document.querySelector('#' + this._sDivId), + $.extend({}, _options, o) + ); + this._uploader.setOptions(glFilepondLocale); + + // need to be activated if catch paste needed + //this.initPasteEditor(); + } + + this.onUploadCompleted = function (sErrorMsg) { + if (sErrorMsg.length) + this._showError(sErrorMsg); + + var $this = this; + if($this._uploader.status != 3) { + this._isUploadsInProgress = false; + this.restoreGhosts(); + if(!this._isErrorShown) { + $('#' + this._sPopupContainerId).dolPopupHide({}); + this.removeFiles(); + } + + if(typeof this._onUpload === 'function') + this._onUpload(this, this._iContentId); + } + } + + this.removeFiles = function () { + oFiles = this._uploader.getFiles(); + for (i = 0; i < oFiles.length; i++){ + this._uploader.removeFile(i); + } + } + + this.cancelAll = function () { + this.removeFiles(); + } + + this.onBeforeUpload = function (params) { + this._isUploadsInProgress = true; + this._clearErrors(); + + if(typeof this._onUploadBefore === 'function') + this._onUploadBefore(this); + } + + this.onProgress = function (params) { + } + + this.onClickCancel = function () { + var $this = this; + if (this._isUploadsInProgress) { + bx_confirm(_t('_sys_uploader_confirm_close_popup'), function() { + $this.removeFiles(); + $this.cancelAll(); + $('#' + $this._sPopupContainerId).dolPopupHide({}); + }); + } else { + $this.removeFiles(); + $('#' + this._sPopupContainerId).dolPopupHide(); + } + + BxDolUploaderBase.prototype._clearErrors.call(this); + } + + this.onShowPopup = function () { + var $this = this; + setTimeout(function () { + $this.focusPasteEditor(); + }, 200); + } + + this.focusPasteEditor = function () { + $('#' + this._sFocusDivId).focus(); + } + + this.initPasteEditor = function () { + var $this = this; + $('#' + this._sFocusDivId).on('paste', function (e) { + var aFiles = []; + if (e.type == 'paste') { + var aItems = (e.clipboardData || e.originalEvent.clipboardData).items; + + for (var i in aItems) { + var oItem = aItems[i]; + if (oItem.kind === 'file') + aFiles.push(oItem.getAsFile()); + } + $this._uploader.addFiles(aFiles); + } + }); + } + + this.showUploaderForm = function() { + this._uploader.browse(); + } + + this.initUploader({ + 'maxFilesize': options.maxFilesize, + 'acceptedFiles': options.acceptedFiles, + 'resizeWidth': options.resizeWidth, + 'resizeHeight': options.resizeHeight, + 'resizeMethod': options.resizeMethod, + 'dictDefaultMessage': options.dictDefaultMessage, + 'dictFileTooBig': options.dictFileTooBig, + 'dictMaxFilesExceeded': options.dictMaxFilesExceeded, + 'dictInvalidFileType': options.dictInvalidFileType, + }); +} + +BxDolUploaderHTML5.prototype = BxDolUploaderBase.prototype; + +function BxDolUploaderSimple(sUploaderObject, sStorageObject, sUniqId, options) +{ + BxDolUploaderHTML5.call(this, sUploaderObject, sStorageObject, sUniqId, options); +} + +BxDolUploaderSimple.prototype = Object.create(BxDolUploaderHTML5.prototype); +BxDolUploaderSimple.prototype.constructor = BxDolUploaderSimple; + +/** + * Crop Image Uploader js class + */ +function BxDolUploaderCrop (sUploaderObject, sStorageObject, sUniqId, options) { + + this.init(sUploaderObject, sStorageObject, sUniqId, options); + + this._eForm = null; + + this.initUploader = function (oOptions) { + + var $this = this; + + var aExt = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + var eCroppie = $("#" + this._sFormContainerId + " .bx-croppie-element").croppie(oOptions); + + $("#" + this._sFormContainerId + ' .bx-crop-rotate').on('click', function(ev) { + eCroppie.croppie('rotate', parseInt($(this).data('deg'))); + }); + + $("#" + this._sFormContainerId + " input[name=f]").on("change", function() { + var input = this; + + if (input.files && input.files[0]) { + + var m = input.files[0].name.match(/\.([A-Za-z0-9]+)$/); + + if (2 != m.length || -1 == aExt.indexOf(m[1].toLowerCase())) { + $(input).replaceWith($(input).val('').clone(true)); + $this._showError(_t('_sys_uploader_crop_wrong_ext')); + return; + } + + $this._clearErrors(); + + var reader = new FileReader() + + reader.onload = function(e) { + eCroppie.croppie('bind', { + url: e.target.result + }); + $("#" + $this._sFormContainerId + " .bx-croppie-element").addClass('ready'); + $("#" + $this._sFormContainerId + " .bx-crop-action").removeClass('bx-btn-disabled'); + $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename', input.files[0].name.replace(/(\.[A-Za-z0-9]+)$/, '.jpg')); + } + reader.readAsDataURL(input.files[0]); + } + }); + + $("#" + this._sFormContainerId + " .bx-crop-upload").on('click', function(ev) { + eCroppie.croppie('result', { + type: 'canvas', + size: 'original', + format: 'jpeg', + quality: '0.85', + }).then(function(resp) { + var fd = new FormData(); + + fd.append("f", dataURItoBlob(resp), $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename')); + $.each(oOptions.bx_form, function (sName) { + fd.append(sName, this); + }); + + $this.onBeforeUpload(fd); + + $.ajax({ + url: sUrlRoot + 'storage_uploader.php', + type: "POST", + processData: false, + contentType: false, + data: fd, + success: function(data) { + eval(data); + }, + error: function() { + $this._showError(_t('_sys_uploader_crop_err_upload')); + } + }) + + }); + }); + + function dataURItoBlob(dataURI) { + var split = dataURI.split(','), + dataTYPE = split[0].match(/:(.*?);/)[1], + binary = atob(split[1]), + array = []; + + for (var i = 0; i < binary.length; i++) + array.push(binary.charCodeAt(i)); + + return new Blob([new Uint8Array(array)], { + type: dataTYPE + }); + } + + + }; +} + +BxDolUploaderCrop.prototype = BxDolUploaderBase.prototype; + + +/** + * Record Video Uploader js class + */ +function BxDolUploaderRecordVideo (sUploaderObject, sStorageObject, sUniqId, options) { + this._camera = null; + this._blob = null; + this._recorder = null; + this._camera_type = 'user'; + + this._audio_bitrate = undefined !== options.audio_bitrate ? parseInt(options.audio_bitrate) : 128000; + this._video_bitrate = undefined !== options.video_bitrate ? parseInt(options.video_bitrate) : 1000000; + + this.init(sUploaderObject, sStorageObject, sUniqId, options); + + this.showUploaderForm = function() { + if (typeof MediaRecorder == 'undefined' || !('requestData' in MediaRecorder.prototype)) { + bx_alert(_t('_sys_uploader_unsupported_browser')); + return; + } + + BxDolUploaderBase.prototype.showUploaderForm.call(this); + } + + this.onBeforeShowPopup = function() { + this._blob = null; + this._camera = null; + this._recorder = null + this._clearErrors(); + + $('#bx-upoloader-recording-preview').hide(); + $('#bx-upoloader-camera-capture').show(); + + $('#' + this._sFormContainerId + ' .bx-uploader-record-video-controls').hide(); + } + + this.switchCamera = function () { + this._camera_type = this._camera_type == 'user' ? 'environment' : 'user'; + + this.releaseCamera(); + this.onShowPopup(); + } + + this.onShowPopup = function () { + var $this = this; + try { + navigator.mediaDevices.getUserMedia({ audio: true, video: {facingMode: this._camera_type} }).then(function(camera) { + $this._camera = camera; + $this.showCameraCapture(); + + $('#' + $this._sFormContainerId + ' .bx-uploader-recording-start').show(); + $('#' + $this._sFormContainerId + ' .bx-uploader-recording-stop').hide(); + $('#' + $this._sFormContainerId + ' .bx-uploader-record-video-controls').show(); + + navigator.mediaDevices.enumerateDevices().then(function(mediaDevices){ + let constraints = navigator.mediaDevices.getSupportedConstraints(); + if ($this.getDevicesNum(mediaDevices) > 1 && typeof constraints.facingMode != 'undefined' && constraints.facingMode) { + $('#' + $this._sFormContainerId + ' .bx-record-camera-switch').show(); + } else { + $('#' + $this._sFormContainerId + ' .bx-record-camera-switch').hide(); + } + }); + + }) + } catch (err) { + $this._showError(_t('_sys_uploader_camera_capture_failed')); + } + } + + this.onClickCancel = function () { + this.releaseCamera(); + + BxDolUploaderBase.prototype.onClickCancel.call(this); + } + + this.startRecording = function() { + this._recorder = RecordRTC(this._camera, { + type: 'video', + audioBitsPerSecond: this._audio_bitrate, + videoBitsPerSecond: this._video_bitrate, + disableLogs: true + }); + + if (!this._recorder) { + this._showError(_t('_sys_uploader_unsupported_browser')); + return; + } + + $("#" + this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").hide(); + $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').hide(); + $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').show(); + + this._recorder.startRecording(); + + this.showCameraCapture(); + } + + this.stopRecording = function(bSubmitWhenReady) { + $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').show(); + $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').hide(); + + var $this = this; + this._recorder.stopRecording(function(){ + $this._blob = $this._recorder.getBlob(); + $this.showRecordingPreview(); + + $this._recorder.destroy(); + $this._recorder = null; + + $("#" + $this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").show(); + if (bSubmitWhenReady) + $this.submitRecording($('#' + $this._sFormContainerId + ' form').get(0)); + }); + } + + this.showCameraCapture = function() { + var video = $('#bx-upoloader-recording-preview').hide().get(0); + if (video.pause !== 'undefined') video.pause(); + video.removeAttribute('src'); + + video = $('#bx-upoloader-camera-capture').show().get(0); + video.srcObject = this._camera; + video.muted = true; + video.volume = 0; + $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html(''); + } + + this.showRecordingPreview = function() { + $('#bx-upoloader-camera-capture').hide(); + $('#bx-upoloader-recording-preview').show().get(0).src = URL.createObjectURL(this._blob); + + var mbytes = (this._blob.size/1024/1024).toFixed(2); + $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html(mbytes + ' ' + _t('_sys_uploader_record_video_mb')); + } + + this.submitRecording = function(form) { + if (this._recorder) { + this.stopRecording(true); + return; + } + + this.onBeforeUpload(form); + + var data = new FormData(form); + if (this._blob != null) data.append("f[]", this._blob, new Date().toISOString() + '.webm'); + + var $this = this; + $.ajax({ + url: $(form).attr('action'), + type: "POST", + data: data, + processData: false, + contentType: false, + success:function(sErrorMsg, textStatus, jqXHR) { + $this.onUploadCompleted(sErrorMsg); + if (!sErrorMsg.length) { + $this.releaseCamera(); + } + }, + }); + } + + this.releaseCamera = function() { + //stop playing recorded file + var video = $('#bx-upoloader-recording-preview').get(0); + if (video.pause != 'undefined') video.pause(); + video.removeAttribute('src'); + + if (this._recorder) { + this._recorder.destroy(); + this._recorder = null; + } + + if (this._camera) { + if (typeof this._camera.stop != 'undefined') this._camera.stop(); + this._camera.getTracks().forEach(function (track) { + if (track.readyState == 'live') { + track.stop(); + } + }); + } + } + + this.getDevicesNum = function(mediaDevices) { + let count = 0; + mediaDevices.forEach(mediaDevice => { + if (mediaDevice.kind === 'videoinput') count++; + }); + return count; + } +} + +BxDolUploaderRecordVideo.prototype = BxDolUploaderBase.prototype; + + +function BxDolImageTweak (unique_id, action_url, content_id, field, js_object, allow_tweak) { + var $this = this; + this._sUniqueId = unique_id; + this._sActionUrl = action_url; + this._sContentId = content_id; + this._sField = field; + this._sJsObject = js_object; + this._bAllowTweak = allow_tweak; + this._oContainerButtons = $('.bx-image-edit-buttons-' + unique_id); + this._oContainerButtons.removeClass('hidden'); + $("#bx-form-input-files-" + this._sUniqueId + "-upload-result").bind( "contentchange", function(){ + $this.uploadComplete($(this)); + }); + + if (this._bAllowTweak){ + this._oContainerButtons.find('.bx-image-edit-buttons-edit').removeClass('hidden'); + } +} + +BxDolImageTweak.prototype.showUploaderForm = function(){ + var $this = this; + $('#bx-form-input-files-' + $this._sUniqueId + '-upload-result').html(''); + eval($this._sJsObject + '.showUploaderForm()'); +} + +BxDolImageTweak.prototype.uploadComplete = function(obj){ + var $this = this; + var sUrl = $this._sActionUrl + "updateImage/" + $this._sField + "/" + $this._sContentId + "/" + obj.html() + "/"; + + $this._oContainerButtons.removeClass('bx-image-edit-buttons-no-image'); + $.post(sUrl, function (aData) { + var sSelImage = ".bx-image-edit-source-" + $this._sUniqueId; + var sSelPlaceholder = ".bx-image-edit-placeholder-" + $this._sUniqueId; + + if ($this._bAllowTweak){ + $(sSelImage).css("background-image", "url(" + aData + ")").css('background-position', '').show(); + + if($(sSelPlaceholder).length > 0) + $(sSelPlaceholder).hide(); + else + $(sSelImage).parents('.bx-base-pofile-cover-image.bx-bpci-holder').removeClass('bx-bpci-holder h-32 lg:h-32').addClass('h-64 lg:h-80') + } + else{ + $(sSelImage).attr("src", aData).show(); + + if($(sSelPlaceholder).length > 0) + $(sSelPlaceholder).hide(); + else + $(sSelImage).parents('.bx-base-pofile-cover-thumb').find('p.bx-base-pofile-unit-thumb').hide(); + } + }); +} + +BxDolImageTweak.prototype.changePosition = function (){ + var $this = this; + $(".bx-image-edit-source-" + $this._sUniqueId).parent().append("
" + _t('_sys_uploader_image_reposition_info') + "
"); + $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').bind('dragover', function(e){ + $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove(); + $(".bx-image-edit-source-" + $this._sUniqueId).css('background-position', " 0px " + e.offsetY / $(e.currentTarget).height() * 100 + '%'); + }); + if (('ontouchstart' in window) || (navigator.msMaxTouchPoints > 0)) { + $('body').css('overflow', 'hidden') + $(".bx-image-edit-source-" + $this._sUniqueId).bind('touchmove', function(e){ + var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; + $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove(); + $(".bx-image-edit-source-" + $this._sUniqueId).css('background-position', " 0px " + touch.pageY / $(e.currentTarget).height() * 100 + '%'); + }); + } + with ($this._oContainerButtons) { + find('.bx-image-edit-buttons-cancel').removeClass('hidden'); + find('.bx-image-edit-buttons-save').removeClass('hidden'); + find('.bx-image-edit-buttons-edit').addClass('hidden'); + find('.bx-image-edit-buttons-upload').addClass('hidden'); + } +} + +BxDolImageTweak.prototype.cancelPosition = function (){ + var $this = this; + $('body').css('overflow', '') + $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove(); + $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').unbind('dragover').unbind('touchmove'); + + with ($this._oContainerButtons) { + find('.bx-image-edit-buttons-cancel').addClass('hidden'); + find('.bx-image-edit-buttons-save').addClass('hidden'); + find('.bx-image-edit-buttons-edit').removeClass('hidden'); + find('.bx-image-edit-buttons-upload').removeClass('hidden'); + } +} + +BxDolImageTweak.prototype.savePosition = function (){ + var $this = this; + $('body').css('overflow', '') + $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove(); + $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').unbind('dragover').unbind('touchmove'); + $(".bx-image-edit-source-" + $this._sUniqueId).removeClass('bx-image-edit-move') + + aPos = $(".bx-image-edit-source-" + $this._sUniqueId).css('background-position').split(' '); + $.post($this._sActionUrl + 'updateImagePosition/' + $this._sContentId + '/' + $this._sField + '/' + aPos[0].replace('%','').replace('px','') + '/' + aPos[1].replace('%','').replace('px','') + '/', function () { + with ($this._oContainerButtons) { + find('.bx-image-edit-buttons-cancel').addClass('hidden'); + find('.bx-image-edit-buttons-save').addClass('hidden'); + find('.bx-image-edit-buttons-edit').removeClass('hidden'); + find('.bx-image-edit-buttons-upload').removeClass('hidden'); + } + }); +} + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolVoteReactions.js b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolVoteReactions.js new file mode 100644 index 0000000000..6a75eada08 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/classes/BxDolVoteReactions.js @@ -0,0 +1,323 @@ +/** + * Copyright (c) UNA, Inc - https://una.io + * MIT License - https://opensource.org/licenses/MIT + * + * @defgroup UnaCore UNA Core + * @{ + */ + +function BxDolVoteReactions(oOptions) +{ + BxDolVote.call(this, oOptions); + this._bQuickMode = oOptions.bQuickMode === undefined ? 0 : oOptions.bQuickMode; // enable 'quick' mode - vote with default reaction when clicked. + + this._iTimeoutShowId = 0; + this._iTimeoutShowDelay = 750; + this._iTimeoutHideId = 0; + this._iTimeoutHideDelay = 1000; + this._fOnVoteIn = null; + this._fOnVoteOut = null; + this._fOnDoPopupIn = null; + this._fOnDoPopupOut = null; + + this._sClassDo = 'bx-vote-do-vote'; + this._sClassDoVoted = 'bx-vote-voted'; +} + +BxDolVoteReactions.prototype = Object.create(BxDolVote.prototype); +BxDolVoteReactions.prototype.constructor = BxDolVoteReactions; + +BxDolVoteReactions.prototype.init = function() +{ + BxDolVote.prototype.init.call(this); + + var $this = this; + var bMobile = bx_check_mq() == 'mobile'; + + if(!this._bQuickMode || !bMobile) + $('#' + this._aHtmlIds['main'] + ' .' + this._sClassDo).hover(function() { + $this.onVoteIn(this); + }, function() { + $this.onVoteOut(this); + }); + else + $('#' + this._aHtmlIds['main'] + ' .' + this._sClassDo).onLongTouch(function(oElement) { + $this.onTouch(oElement); + }); +}; + +BxDolVoteReactions.prototype.vote = function(oLink, iValue, sReaction, onComplete) +{ + var $this = this; + var oParams = this._getDefaultParams(); + oParams['action'] = 'Vote'; + oParams['value'] = iValue; + oParams['reaction'] = sReaction; + + if(this._iTimeoutShowId) + clearTimeout(this._iTimeoutShowId); + + $('#' + this._aHtmlIds['do_popup']).dolPopupHide({}); + + $.post( + this._sActionsUrl, + oParams, + function(oData) { + if(oData && oData.message != undefined) + bx_alert(oData.message, function() { + $this.onVote(oLink, oData, onComplete); + }); + else + $this.onVote(oLink, oData, onComplete); + }, + 'json' + ); +}; + +BxDolVoteReactions.prototype.onVote = function (oLink, oData, onComplete) +{ + var $this = this; + + if(oData && oData.code != 0) + return; + + oLink = $('.' + this._aHtmlIds['main'] + ' .' + this._sClassDo); + + //--- Update Do button. + oLink.each(function() { + if(oData && oData.label_use) + switch(oData.label_use) { + case 'icon': + if($(this).find('.sys-action-do-icon .sys-icon').length && !$(oData.label_icon).hasClass('sys-icon')) + $(this).find('.sys-action-do-icon .sys-icon').attr('class', 'sys-icon ' + oData.label_icon).html(''); + else + $(this).find('.sys-action-do-icon').html(oData.label_icon); + break; + + case 'emoji': + $(this).find('.sys-action-do-icon .sys-icon').attr('class', 'sys-icon sys-icon-emoji').html(oData.label_emoji); + break; + + case 'image': + $(this).find('.sys-action-do-icon').html(oData.label_image); + break; + } + + if(oData && oData.label_title) { + $(this).attr('title', oData.label_title); + $(this).find('.sys-action-do-text').html(oData.label_title); + } + + if(oData && oData.label_click) + $(this).attr('onclick', 'javascript:' + oData.label_click) + + if(oData && oData.disabled) + $(this).removeAttr('onclick').addClass($(this).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-vote-disabled'); + + if(oData && oData.voted != undefined) + $(this).toggleClass($this._sClassDoVoted, oData.voted); + }); + + //--- Update Counter. + var oCounter = this._getCounter(oLink); + if(oCounter && oCounter.length > 0) { + oCounter.filter('.' + oData.reaction).each(function() { + $(this).html(oData.countf).toggleClass('bx-vc-hidden', !oData.count); + }); + + //--- Update Total. + if(oData.total) + oCounter.filter('.total-count').each(function() { + $(this).html(oData.total.countf).toggleClass('bx-vc-hidden', !oData.total.count); + }); + + //--- Show counter. + oCounter.filter('.' + oData.reaction).each(function() { + $(this).parents('.' + $this._sSP + '-counter-wrapper:first').toggleClass('bx-vc-hidden', !oData.count && !oData.total.count); + }); + } + + if(typeof onComplete == 'function') + onComplete(oLink, oData); +}; + +BxDolVoteReactions.prototype.onVoteIn = function(oLink) +{ + var $this = this; + + if($(oLink).hasClass(this._sClassDoVoted)) + return; + + if(this._iTimeoutHideId) + clearTimeout(this._iTimeoutHideId); + + var oPopup = this.getDoPopup(); + if(oPopup !== false) + return; + + this._iTimeoutShowId = setTimeout(function() { + $this.toggleDoPopup(oLink, $(oLink).attr('bx-vote-value')); + }, this._iTimeoutShowDelay); +}; + +BxDolVoteReactions.prototype.onVoteOut = function(oLink) +{ + if($(oLink).hasClass(this._sClassDoVoted)) + return; + + if(this._iTimeoutShowId) + clearTimeout(this._iTimeoutShowId); + + this.hideDoPopup(); +}; + +BxDolVoteReactions.prototype.onTouch = function(oLink) +{ + if($(oLink).hasClass(this._sClassDoVoted)) + return; + + var oPopup = this.getDoPopup(); + if(oPopup !== false) + return; + + this.toggleDoPopup(oLink, $(oLink).attr('bx-vote-value'), { + closeOnOuterClick: false + }); +}; + +BxDolVoteReactions.prototype.getDoPopup = function() +{ + var oPopup = $('#' + this._aHtmlIds['do_popup'] + ':visible'); + return oPopup.length > 0 && oPopup.hasClass('bx-popup-applied') ? oPopup : false; +}; + +BxDolVoteReactions.prototype.toggleDoPopup = function(oLink, iValue, oOptions) +{ + var $this = this; + var oParams = this._getDefaultParams(); + oParams['action'] = 'GetDoVotePopup'; + + if(this._iTimeoutShowId) + clearTimeout(this._iTimeoutShowId); + + oOptions = oOptions || {}; + oOptions = $.extend({}, { + id: {value: this._aHtmlIds['do_popup'], force: true}, + url: bx_append_url_params(this._sActionsUri, oParams), + value: iValue, + onShow: function(oPopup) { + $this.onDoPopupShow(oPopup); + }, + onHide: function(oPopup) { + $this.onDoPopupHide(oPopup); + }, + cssClass: 'bx-popup-vote-reactions ' + $this._sSystem.replaceAll('_', '-') + }, oOptions); + + $(oLink).dolPopupAjax(oOptions); +}; + +BxDolVoteReactions.prototype.hideDoPopup = function() +{ + var oPopup = this.getDoPopup(); + if(!oPopup) + return; + + this._iTimeoutHideId = setTimeout(function() { + oPopup.dolPopupHide(); + }, this._iTimeoutHideDelay); +}; + +BxDolVoteReactions.prototype.onDoPopupShow = function(oPopup) +{ + var $this = this; + + this._fOnDoPopupIn = function() { + $this.onDoPopupIn(this); + }; + + this._fOnDoPopupOut = function() { + $this.onDoPopupOut(this); + }; + + $(oPopup).hover(this._fOnDoPopupIn, this._fOnDoPopupOut); +}; + +BxDolVoteReactions.prototype.onDoPopupHide = function(oPopup) +{ + $(oPopup).unbind('mouseenter', this._fOnDoPopupIn).unbind('mouseleave', this._fOnDoPopupOut); +}; + +BxDolVoteReactions.prototype.onDoPopupIn = function(oPopup) +{ + if(this._iTimeoutHideId) + clearTimeout(this._iTimeoutHideId); +}; + +BxDolVoteReactions.prototype.onDoPopupOut = function(oPopup) +{ + this.hideDoPopup(); +}; + +BxDolVoteReactions.prototype.toggleByPopup = function(oLink, sReaction) +{ + var oParams = this._getDefaultParams(); + oParams['action'] = 'GetVotedBy'; + if(sReaction) + oParams['reaction'] = sReaction; + + $(oLink).dolPopupAjax({ + id: {value: this._aHtmlIds['by_popup'], force: true}, + url: bx_append_url_params(this._sActionsUri, oParams), + removeOnClose: true + }); +}; + +BxDolVoteReactions.prototype.changeVotedBy = function(oLink, sReaction) +{ + var oContent = $('#' + this._aHtmlIds['by_popup'] + ' .' + this._sSP + '-bls-content'); + if(oContent.length > 0) + oContent.children(':visible').hide().siblings('.' + this._sSP + '-bl-' + sReaction).show(); +}; + +BxDolVoteReactions.prototype._getCounter = function(oElement) +{ + var oCounter = BxDolVote.prototype._getCounter.call(this, oElement); + if(oCounter && oCounter.length > 0) + return oCounter; + + return $('.' + this._aHtmlIds['counter']).find('.' + this._sSP + '-counter'); +}; + +(function($) { + $.fn.onLongTouch = function(fCallback) { + return this.each(function() { + var oSource = this; + var iTimeoutId = null; + + this.addEventListener('touchstart', function(e) { + iTimeoutId = setTimeout(function() { + iTimeoutId = null; + e.stopPropagation(); + fCallback(oSource); + }, 500); + }); + + this.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }); + + this.addEventListener('touchend', function() { + if(iTimeoutId) + clearTimeout(iTimeoutId); + }); + + this.addEventListener('touchmove', function() { + if(iTimeoutId) + clearTimeout(iTimeoutId); + }); + }); + }; +})(jQuery); + +/** @} */ diff --git a/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/functions.js b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/functions.js new file mode 100644 index 0000000000..34b4a18630 --- /dev/null +++ b/upgrade/files/14.0.0.B2-14.0.0.RC1/files/inc/js/functions.js @@ -0,0 +1,1622 @@ +/** + * Copyright (c) UNA, Inc - https://una.io + * MIT License - https://opensource.org/licenses/MIT + * + * @defgroup UnaCore UNA Core + * @{ + */ + +function processJsonData(oData) { + var fContinue = function(oData) { + if(oData && oData.reload != undefined && parseInt(oData.reload) == 1) + document.location = document.location; + + if(oData && oData.redirect != undefined && oData.redirect.length != 0) + document.location = oData.redirect; + + if(oData && oData.popup != undefined) { + var oPopup = null; + var oOptions = { + fog: { + color: '#fff', + opacity: .7 + }, + closeOnOuterClick: false + }; + + if(typeof(oData.popup) == 'object') { + oOptions = $.extend({}, oOptions, oData.popup.options); + oPopup = $(oData.popup.html); + } + else + oPopup = $(oData.popup); + + if ('undefined' !== typeof(bx_editor_remove_all)) + bx_editor_remove_all($('#' + oPopup.attr('id'))); + + $('#' + oPopup.attr('id')).remove(); + oPopup.hide().prependTo('body').bxProcessHtml().dolPopup(oOptions); + } + + if(oData && oData.form != undefined && oData.form_id != undefined) + $('form#' + oData.form_id).replaceWith(oData.form).bxProcessHtml(); + + if (oData && oData.eval != undefined) + eval(oData.eval); + }; + + if(oData && oData.object != undefined && oData.grid != undefined) + if(glGrids[oData.object] != undefined) + glGrids[oData.object].processJson(oData); + + if(oData && oData.message != undefined && oData.message.length != 0) + bx_alert(oData.message, function() { + fContinue(oData); + }); + else if(oData && oData.msg != undefined && oData.msg.length != 0) + bx_alert(oData.msg, function() { + fContinue(oData); + }); + else + fContinue(oData); +}; + +function getHtmlData( elem, url, callback, method, confirmation, values, loading) +{ + var fPerform = function() { + // in most cases it is element ID, in other cases - object of jQuery + if (typeof elem == 'string') + elem = '#' + elem; // create selector from ID + + var $block = $(elem); + + var blockPos = $block.css('position'); + + $block.css('position', 'relative'); // set temporarily for displaying "loading icon" + + if ('undefined' === typeof(loading) || loading) { + bx_loading_content($block, true); + var $loadingDiv = $block.find('.bx-loading-ajax'); + + var iLeftOff = parseInt(($block.innerWidth() / 2.0) - ($loadingDiv.outerWidth() / 2.0)); + var iTopOff = parseInt(($block.innerHeight() / 2.0) - ($loadingDiv.outerHeight())); + if (iTopOff<0) iTopOff = 0; + + $loadingDiv.css({ + position: 'absolute', + left: iLeftOff, + top: iTopOff + }); + } + + if (undefined != method && (method == 'post' || method == 'POST')) { + if (typeof values == 'undefined') + values = {}; + $.post(url, values, function(data) { + $block.html(data); + $block.css('position', blockPos).bxProcessHtml(); + + if (typeof callback == 'function') + callback.apply($block); + }); + } + else { + $block.load(url + '&_r=' + Math.random(), function() { + $(this).css('position', blockPos).bxProcessHtml(); + + if (typeof callback == 'function') + callback.apply(this); + }); + } + }; + + if (typeof(confirmation) != 'undefined' && confirmation) + bx_confirm(_t('_Are_you_sure'), fPerform); + else + fPerform(); +} + +function loadDynamicBlockAutoPaginate (e, iStart, iPerPage, sAdditionalUrlParams, sStartParamName, sPerPageParamName) { + + sUrl = location.href; + sStartParamName = typeof sStartParamName !== 'undefined' ? sStartParamName : 'start'; + sPerPageParamName = typeof sPerPageParamName !== 'undefined' ? sPerPageParamName : 'per_page'; + + sUrl = sUrl.replace(/start=\d+/, '').replace(/per_page=\d+/, '').replace(/[&\?]+$/, ''); + sUrl = bx_append_url_params(sUrl, sStartParamName + '=' + parseInt(iStart) + '&' + sPerPageParamName + '=' + parseInt(iPerPage)); + if ('undefined' != typeof(sAdditionalUrlParams)) + sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams); + + if ($(e).parents('.bx-search-result-block-pagination').length > 0){ + $([document.documentElement, document.body]).animate({ + scrollTop: $(e).parents('.bx-search-result-block-pagination').first().offset().top + }, 500); + } + return loadDynamicBlockAuto(e, sUrl); +} + +function loadDynamicBlockAutoSort (e, sort, sAdditionalUrlParams) { + + sUrl = location.href; + console.log(sort); + + sUrl = sUrl.replace(/sort=\d+/, '').replace(/[&\?]+$/, ''); + sUrl = bx_append_url_params(sUrl, 'sort' + '=' + sort); + if ('undefined' != typeof(sAdditionalUrlParams)) + sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams); + + if ($(e).parents('.bx-search-result-block-pagination').length > 0){ + $([document.documentElement, document.body]).animate({ + scrollTop: $(e).parents('.bx-search-result-block-pagination').first().offset().top + }, 500); + } + 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:not(.no-dynamic)'); + var sId = oContainer.attr('id'); + + if ('undefined' == typeof(sUrl)) + sUrl = location.href; + + if (!sId || !sId.length) + return false; + + var aMatches = sId.match(/\-(\d+)$/); + if (!aMatches || !aMatches[1]) + return false; + + var oPaginate = oContainer.find('.bx-paginate'); + if(oPaginate.length > 0) { + var iStart = parseInt(oPaginate.attr('bx-data-start')); + var iPerPage = parseInt(oPaginate.attr('bx-data-perpage')); + + var oParams = {}; + if(!isNaN(iStart) && sUrl.indexOf('start') == -1) + oParams.start = iStart; + if(!isNaN(iPerPage) && sUrl.indexOf('per_page') == -1) + oParams.per_page = iPerPage; + + sUrl = bx_append_url_params(sUrl, oParams); + } + + loadDynamicBlock(parseInt(aMatches[1]), sUrl); + return true; +} + +function loadDynamicBlock(iBlockID, sUrl) { + + var oCallback = null; + if($('#bx-page-block-' + iBlockID + ' .bx-base-unit-showcase-wrapper').length){ + oCallback = bx_showcase_view_init; + } + getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID), oCallback); + return true; +} + +function loadDynamicPopupBlock(iBlockID, sUrl) { + if (!$('#dynamicPopup').length) { + $('').prependTo('body'); + } + + $('#dynamicPopup').load( + (sUrl + '&dynamic=popup&pageBlock=' + iBlockID), + function() { + $(this).dolPopup({ + left: 0, + top: 0 + }); + } + ); +} + +function closeDynamicPopupBlock() { + $('#dynamicPopup').dolPopupHide(); +} + +function onAsyncBlockLoad() { + var oBlock = $(this); + + if(!oBlock.html()) + oBlock.remove(); +} + +/** + * 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(); + $('').prependTo('body').load( + sUrl.match('^http[s]{0,1}:\/\/') ? sUrl : sUrlRoot + sUrl, + function() { + $(this).dolPopup(oPopupOptions); + } + ); +} + +function bx_loading_svg(sType, sClass) { + sClass = sClass != undefined && sClass.length > 0 ? sClass : ''; + + if(sUseSvgLoading != undefined && sUseSvgLoading.length > 0) + return sUseSvgLoading.replace(new RegExp('__type__','g'), sType).replace(new RegExp('__class__','g'), sClass); + + var s = ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + s += ''; + return s; +} + +function bx_loading_animate (e, aOptions) { + e = $(e); + if (!e.length) + return false; + if (e.find('.bx-sys-spinner').length) + return false; + return new Spinner(aOptions).spin(e.get(0)); +} + +function bx_loading_btn (oElement, bEnable) { + var oButton = $(oElement); + var sClassHeight = oButton.hasClass('bx-btn-small') ? 'bx-btn-small-height' : 'bx-btn-height'; + + if (oButton.children('div').size()) + oButton = oButton.children('div').first(); + + if(!bEnable) + oButton.find('.bx-loading-ajax-btn').remove(); + else if (!oButton.find('.bx-loading-ajax-btn').length) + oButton.append('' + (bUseSvgLoading ? bx_loading_svg('colored', sClassHeight) : '') + ''); + + if(!bUseSvgLoading) + bx_loading_animate(oButton.find('.bx-loading-ajax-btn'), aSpinnerSmallOpts); +} + +function bx_loading_content (oElement, bEnable, isReplace) { + var oBlock = $(oElement); + var oLoading = $('
' + (bUseSvgLoading ? bx_loading_svg('colored') : '') + '
'); + + if(!bEnable) + oBlock.find(".bx-loading-ajax").remove(); + else if(!oBlock.find('.bx-loading-ajax').length) { + if('undefined' != typeof(isReplace) && isReplace) + oBlock.html(oLoading.addClass('static')); + else + oBlock.append(oLoading); + + if(!bUseSvgLoading) + bx_loading_animate(oBlock.find('.bx-loading-ajax'), aSpinnerOpts); + } +} + +function bx_loading (elem, b) { + + if (typeof elem == 'string') + elem = '#' + elem; + + var block = $(elem); + + if (block.hasClass('bx-btn')) { + bx_loading_btn (block, b); + return; + } + + if (1 == b || true == b) { + + bx_loading_content(block, b); + + e = block.find(".bx-loading-ajax"); + e.css('left', parseInt(block.width()/2.0 - e.width()/2.0)); + + var he = e.outerHeight(); + var hc = block.outerHeight(); + + if (block.css('position') != 'relative' && block.css('position') != 'absolute') { + if (!block.data('css-save-position')) + block.data('css-save-position', block.css('position')); + block.css('position', 'relative'); + } + + if (hc > he) { + e.css('top', parseInt(hc/2.0 - he/2.0)); + } + + if (hc < he) { + if (!block.data('css-save-min-height')) + block.data('css-save-min-height', block.css('min-height')); + block.css('min-height', he); + e.css('top', 0); + } + + } else { + + if (block.data('css-save-position')) + block.css('position', block.data('css-save-position')); + + if (block.data('css-save-min-height')) + block.css('min-height', block.data('css-save-min-height')); + + bx_loading_content(block, b); + + } + +} + + +/** + * Center content with floating blocks. + * sSel - jQuery selector of content to be centered + * sBlockSel - jquery selector of blocks + */ +function bx_center_content (sSel, sBlockStyle, bListenOnResize) { + + var sId; + if ($(sSel).parent().hasClass('bx-center-content-wrapper')) { + sId = $(sSel).parent().attr('id'); + } else { + sId = 'id' + (new Date()).getTime(); + $(sSel).wrap('
'); + } + + 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)), cssClass: 'bx-popup-menu'}, 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) { + if ($(jSel + ':visible').length) + $(jSel).dolPopupHide(); + else { + var options = options || {}; + var o = $.extend({}, $.fn.dolPopupDefaultOptions, options, { + pointer: e != undefined && $(e).length != 0 ? {el:$(e)} : false, + cssClass: 'bx-popup-menu', + onShow: function(oPopup) { + oPopup.find('a').each(function () { + $(this).off('click.bx-popup-menu'); + $(this).on('click.bx-popup-menu', function() { + $(jSel).dolPopupHide(); + }); + }); + } + }); + + $(jSel).dolPopup(o); + } +} + +/** + * Show pointer popup with menu from URL. + * @param sObject - menu object name + * @param oElement - element to show popup at + * @param oOptions - popup options + * @param oVars - additional GET variables + */ +function bx_menu_slide (sObject, oElement, sPosition, oOptions, oVars) { + var oVars = oVars || {}; + var oOptions = oOptions || {}; + oOptions = $.extend({}, {parent: 'body', container: '.bx-sliding-menu-content'}, oOptions); + + var sId = ''; + var sIdSel = ''; + var sIdPfx = 'bx-sliding-menu-wrapper-'; + if(typeof(oOptions.id) != 'undefined') + switch(typeof(oOptions.id)) { + case 'string': + sId = sIdPfx + oOptions.id; + break; + + case 'object': + sId = typeof(oOptions.id.force) != 'undefined' && oOptions.id.force ? oOptions.id.value : sIdPfx + oOptions.id.value; + break; + } + else + sId = sIdPfx + parseInt(2147483647 * Math.random()); + + sIdSel = '#' + sId; + + //--- If slider exists + if ($(sIdSel).length) { + bx_menu_slide_inline (sIdSel, oElement, sPosition); + return; + } + //--- If slider doesn't exists + else { + var oMenuLoading = $('#bx-sliding-menu-loading'); + if(!oMenuLoading || !oMenuLoading.length) + return; + + $('').addClass(oMenuLoading.attr('class')).appendTo(oOptions.parent); + + var oLoading = $(sIdSel + ' .bx-sliding-menu-loading'); + bx_loading_content(oLoading, true, true); + + bx_menu_slide_inline (sIdSel, oElement, sPosition); + + var fOnLoad = function() { + bx_loading_content(oLoading, false); + + $(sIdSel).bxProcessHtml().show(); + }; + + $(sIdSel).find(oOptions.container).load(bx_append_url_params('menu.php', $.extend({o:sObject}, oVars)), function () { + var f = function () { + if($(sIdSel).find('img').length > 0 && !$(sIdSel).find('img').get(0).complete) + $(sIdSel).find('img').load(fOnLoad); + else + fOnLoad(); + }; + setTimeout(f, 100); // TODO: better way is to check if item is animating before positioning it in the code where popup is positioning + }); + } + +} + +/** + * Show sliding menu from existing HTML. + * @param sMenu - jQuery object or selector for html to show in popup + * @param e - element to click to open/close slider + * @param sPosition - 'block' for sliding menu in blocks, 'site' - for sliding main menu + */ +function bx_menu_slide_inline (sMenu, e, sPosition) { + var options = options || {}; + var eSlider = $(sMenu); + + if ('undefined' !== typeof (bx_menu_slide_inline_custom)) { + return bx_menu_slide_inline_custom(sMenu, e, sPosition); + } + + if ('undefined' == typeof(e)) + e = eSlider.data('data-control-btn'); + + var eIcon = $(e).find('.sys-icon-a'); + + var fPositionElement = function (oElement) { + eSlider.css({ + position: 'absolute', + top: oElement.outerHeight(true), + left: 0 + }); + }; + + var fPositionBlock = function () { + var eBlock = eSlider.parents('.bx-page-block-container'); + eSlider.css({ + position: 'absolute', + top: eBlock.find('.bx-db-header').outerHeight(true), + left: 0 + //width: eBlock.width() TODO: Commited to test. Remove if everything is OK. + }); + }; + + var fPositionSite = function () { + var eToolbar = $('#bx-toolbar'); + eSlider.css({ + position: 'fixed', + top: eToolbar.outerHeight(true) + (typeof iToolbarSubmenuTopOffset != 'undefined' ? parseInt(iToolbarSubmenuTopOffset) : 0), + left: 0 + }); + }; + + var fClose = function () { + if (eIcon.length) + (new Marka(eIcon[0])).set(eIcon.attr('data-icon-orig')); + eSlider.slideUp() + }; + + var fOpen = function () { + if (eIcon.length) { + eIcon.attr('data-icon-orig', eIcon.attr('data-icon')); + (new Marka(eIcon[0])).set('times'); + } + + if(typeof sPosition == 'object') + fPositionElement(sPosition); + else if(typeof sPosition == 'string' && sPosition == 'block') + fPositionBlock(); + else + fPositionSite(); + + eSlider.slideDown(); + + eSlider.data('data-control-btn', e); + }; + + if ('undefined' == typeof(sPosition)) + sPosition = 'block'; + + if (eSlider.is(':visible')) { + fClose(); + + $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu'); + $(window).off('resize.bx-sliding-menu'); + } + else { + bx_menu_slide_close_all_opened(); + + $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu'); + $(window).off('resize.bx-sliding-menu'); + + fOpen(); + eSlider.find('a').each(function () { + $(this).off('click.bx-sliding-menu'); + $(this).on('click.bx-sliding-menu', function () { + fClose(); + }); + }); + + setTimeout(function () { + + var iWidthPrev = $(window).width(); + $(window).on('resize.bx-sliding-menu', function () { + if ($(this).width() == iWidthPrev) + return; + iWidthPrev = $(this).width(); + bx_menu_slide_close_all_opened(); + }); + + $(document).on('click.bx-sliding-menu touchend.bx-sliding-menu', function (event) { + if ($(event.target).parents('.bx-sliding-menu, .bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || $(event.target).filter('.bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || e === event.target) + event.stopPropagation(); + else + bx_menu_slide_close_all_opened(); + }); + + }, 10); + } +} + +/** + * Close all opened sliding menus + */ +function bx_menu_slide_close_all_opened () { + $('.bx-sliding-menu:visible, .bx-sliding-menu-main:visible, .bx-popup-slide-wrapper:visible').each(function () { + bx_menu_slide_inline('#' + this.id); + }); +} + +/* + * Note. oData.count_old and oData.count_new are also available and can be checked or used in notification popup. + */ +function bx_menu_show_live_update(oData) { + var sSelectorAddon = '.bx-menu-item-addon'; + + //--- Update Child Menu Item + if(oData.mi_child) { + var oMenuItem = $('.bx-menu-object-' + oData.mi_child.menu_object + ' .bx-menu-item-' + oData.mi_child.menu_item); + var oMenuItemAddon = oMenuItem.find(sSelectorAddon); + + if(oMenuItemAddon.length > 0) + oMenuItemAddon.html(oData.count_new); + else + oMenuItem.append(oData.code.replace('{count}', oData.count_new)); + + //+++ Check for 0 value + oMenuItemAddon = oMenuItem.find(sSelectorAddon); + if(parseInt(oData.count_new) > 0) + oMenuItemAddon.show(); + else + oMenuItemAddon.hide(); + } + + //--- Update Parent Menu Item + if(oData.mi_parent) { + var oMenuItem = $('.bx-menu-object-' + oData.mi_parent.menu_object + ' .bx-menu-item-' + oData.mi_parent.menu_item); + var oMenuItemAddon = oMenuItem.find(sSelectorAddon); + + var sMediaType = bx_check_mq(); + if(sMediaType == 'mobile') + sMediaType = 'phone'; + + var iSum = 0; + $('.bx-media-' + sMediaType + ' .bx-menu-object-' + oData.mi_child.menu_object + ' .bx-menu-item:not(.bx-def-media-' + sMediaType + '-hide) .bx-menu-item-addon').each(function() { + iValue = parseInt($(this).html()); + if(iValue && iValue > 0) + iSum += iValue; + }); + + if(oMenuItemAddon.length > 0) + oMenuItemAddon.html(iSum); + else + oMenuItem.append(oData.code.replace('{count}', iSum)); + + //+++ Check for 0 value + oMenuItemAddon = oMenuItem.find(sSelectorAddon); + if(iSum > 0) + oMenuItemAddon.show(); + else + oMenuItemAddon.hide(); + } +} + +function bx_menu_show_more_less(oLink, sMenu, sSelectorParent) { + if(!sSelectorParent) + sSelectorParent = 'ul'; + + var sClass = 'bx-mi-hidden'; + $(oLink).parents(sSelectorParent + ':first').find('.bx-mi-aux, .bx-psmi-show-more, .bx-psmi-show-less').toggleClass(sClass); + + $.get(sUrlRoot + 'menu.php', { + o: sMenu, + a: 'set_collapsed', + v: $(oLink).parents(sSelectorParent + ':first').find('.bx-mi-aux').hasClass(sClass) ? 1 : 0 + }); +} + +function bx_menu_toggle_contents(oLink, sSelectorParent) { + if(!sSelectorParent) + sSelectorParent = '.bx-menu-contents'; + + var sClass = 'bx-m-collapsed'; + var oParent = $(oLink).parents(sSelectorParent + ':first'); + + oParent.toggleClass(sClass); + oParent.siblings('.bx-menu-content').toggle(); + + return false; +} + +function bx_menu_toggle(oLink, sMenu, sMenuItem, sSelectorParent) { + if(!sSelectorParent) + sSelectorParent = 'li'; + + var sClass = 'bx-mi-collapsed'; + var oParent = $(oLink).parents(sSelectorParent + ':first'); + + oParent.toggleClass(sClass); + + $.get(sUrlRoot + 'menu.php', { + o: sMenu, + a: 'set_collapsed_submenu', + i: sMenuItem, + v: oParent.hasClass(sClass) ? 1 : 0 + }); + + return false; +} + +function bx_menu_followings_load_more(oLink, sMenu, sContextModule, iStart, iPerPage) { + var oLoadingParent = $(oLink).length ? $(oLink).parents('.bx-menu-item:first') : $('body'); + + bx_loading(oLoadingParent, true); + + $.get(sUrlRoot + 'menu.php', { + o: sMenu, + a: 'load_more', + v: sContextModule, + start: iStart, + per_page: iPerPage + }, function(oData) { + bx_loading(oLoadingParent, false); + + if(oData && oData.content != undefined) + $(oLink).parents('.bx-menu-subitem:first').replaceWith(oData.content); + }, 'json'); +} + +/** + * Set ACL level for specified profile + * @param iProfileId - profile id to set acl level for + * @param iAclLevel - acl level id to assign to a given rofile + */ +function bx_set_acl_level (iProfileId, iAclLevel, mixedLoadingElement) { + var bBulk = !$.isNumeric(iProfileId); + var iAclCard = !bBulk && $('#sys-acl-profile-' + iProfileId).length > 0 ? 1 : 0; + + var bLoading = typeof(mixedLoadingElement) != 'undefined'; + if(bLoading) + bx_loading($(mixedLoadingElement), true); + + $.post(sUrlRoot + 'menu.php', {o:'sys_set_acl_level', profile_id: iProfileId, level_id: iAclLevel, card:iAclCard}, function(oData) { + bx_on_set_acl_level(oData, mixedLoadingElement); + }, 'json'); +} + +function bx_on_set_acl_level(oData, oLoadingElement) { + if(typeof(oLoadingElement) != 'undefined') + bx_loading($(oLoadingElement), false); + + if(oData.msg != undefined && oData.msg.length) { + bx_alert(oData.msg); + return; + } + + if($(oLoadingElement).hasClass('bx-popup-applied')) + $(oLoadingElement).dolPopupHide().remove(); + + if(typeof(oData.card) == 'object') + for(var iField in oData.card) { + var oCard = $(oData.card[iField]); + $('#' + oCard.attr('id')).replaceWith(oCard.bxTime()); + } +} + +/** + * Set badge for specified content + * @param sModule - current module for content + * @param iContentId - content id to set badge for + * @param iBadgeId - badge id to assign to a given content + */ +function bx_set_badge (sModule, iContentId, iBadgeId, mixedLoadingElement) { + + var bLoading = typeof(mixedLoadingElement) != 'undefined'; + if(bLoading) + bx_loading($(mixedLoadingElement), true); + $.post(sUrlRoot + 'menu.php', {o:'sys_set_badges', module: sModule, content_id: iContentId, badge_id: iBadgeId}, function(oData) { + if(bLoading) + bx_loading($(mixedLoadingElement), false); + + if(oData.msg != undefined && oData.msg.length) { + bx_alert(oData.msg); + return; + } + + if($(mixedLoadingElement).hasClass('bx-popup-applied')) + $(mixedLoadingElement).dolPopupHide().remove(); + + if(typeof(oData.card) == 'object') + for(var iField in oData.card) { + var oCard = $(oData.card[iField]); + $('#' + oCard.attr('id')).replaceWith(oCard); + } + + $('.bx-base-bages-container').html(oData.html); + + }, 'json'); +} + +function bx_get_notes(oSource, sModule, iContentId, oOptions, oVars) { + var oOptions = oOptions || {}; + var oVars = oVars || {}; + + $.post( + sUrlRoot + 'modules/?r=' + sModule + '/get_notes/', + $.extend({content_id: iContentId}, oVars), + function(oData) { + if(oData && oData.popup != undefined) { + if(typeof(oData.popup) == 'string') + oData.popup = {html: oData.popup, options: {}}; + + oData.popup.options = $.extend({}, $.fn.dolPopupDefaultOptions, oData.popup.options, { + id: sModule + '_notes_' + iContentId, + closeOnOuterClick: false, + removeOnClose: true, + onBeforeShow: function(oPopup) { + $(oPopup).find('.bx-popup-element-close').removeClass('bx-def-media-desktop-hide bx-def-media-tablet-hide'); + } + }, oOptions); + } + + processJsonData(oData); + }, + 'json' + ); +} + +function bx_approve(oSource, sModule, iContentId, oOptions, oVars) { + var oOptions = oOptions || {}; + var oVars = oVars || {}; + + var fPopupInit = function(oData) { + if(oData && oData.popup != undefined) { + if(typeof(oData.popup) == 'string') + oData.popup = {html: oData.popup, options: {}}; + + oData.popup.options = $.extend({}, $.fn.dolPopupDefaultOptions, oData.popup.options, { + id: sModule + '_approve_' + iContentId, + closeOnOuterClick: false, + removeOnClose: true, + onBeforeShow: function(oPopup) { + $(oPopup).find('.bx-popup-element-close').removeClass('bx-def-media-desktop-hide bx-def-media-tablet-hide'); + + var oForm = $(oPopup).find('form'); + if(oForm.length > 0) { + oForm.ajaxForm({ + dataType: 'json', + beforeSubmit: function(aFormData, oForm, oOptions) { + bx_loading(oForm, true); + }, + success: function(oData) { + $('.bx-popup-applied:visible').dolPopupHide(); + + fPopupInit(oData); + + processJsonData(oData); + } + }); + } + } + }, oOptions); + } + }; + + $.post( + sUrlRoot + 'modules/?r=' + sModule + '/approve/', + $.extend({content_id: iContentId}, oVars), + function(oData) { + fPopupInit(oData); + + processJsonData(oData); + }, + 'json' + ); +} + +function validateLoginForm(eForm) { + return true; +} + +/** + * convert