diff --git a/Gruntfile.js b/Gruntfile.js index 70f192d1ea..7c657beb6f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -176,6 +176,8 @@ module.exports = function (grunt) { 'clear_cache.php', 'upgrade.php', 'extension.php', + 'websocket.php', + 'auth_token_check.php', 'index.php', 'LICENSE.txt', '.htaccess', diff --git a/application/Espo/Core/Loaders/WebSocketSubmission.php b/application/Espo/Core/Loaders/WebSocketSubmission.php new file mode 100644 index 0000000000..c9d0fe6263 --- /dev/null +++ b/application/Espo/Core/Loaders/WebSocketSubmission.php @@ -0,0 +1,40 @@ +getContainer()->get('config') + ); + } +} diff --git a/application/Espo/Core/WebSocket/Pusher.php b/application/Espo/Core/WebSocket/Pusher.php new file mode 100644 index 0000000000..5f8d72c775 --- /dev/null +++ b/application/Espo/Core/WebSocket/Pusher.php @@ -0,0 +1,237 @@ +categoryList = $categoryList; + } + + public function onSubscribe(ConnectionInterface $connection, $topic) + { + $topicId = $topic->getId(); + if (!$topicId) return; + + if (!$this->isCategoryAllowed($topicId)) return; + + $connectionId = $connection->resourceId; + + $userId = $this->getUserIdByConnection($connection); + if (!$userId) return; + + if (!isset($this->connectionIdTopicIdListMap[$connectionId])) $this->connectionIdTopicIdListMap[$connectionId] = []; + + if (!in_array($topicId, $this->connectionIdTopicIdListMap[$connectionId])) { + echo "add topic {$topicId} for user {$userId}\n"; + $this->connectionIdTopicIdListMap[$connectionId][] = $topicId; + } + } + + public function onUnSubscribe(ConnectionInterface $connection, $topic) + { + $topicId = $topic->getId(); + if (!$topicId) return; + + if (!$this->isCategoryAllowed($topicId)) return; + + $connectionId = $connection->resourceId; + + $userId = $this->getUserIdByConnection($connection); + if (!$userId) return; + + if (isset($this->connectionIdTopicIdListMap[$connectionId])) { + $index = array_search($topicId, $this->connectionIdTopicIdListMap[$connectionId]); + if ($index !== false) { + echo "remove topic {$topicId} for user {$userId}\n"; + $this->connectionIdTopicIdListMap[$connectionId] = array_splice($this->connectionIdTopicIdListMap[$connectionId], $index, 1); + } + } + } + + protected function isCategoryAllowed($category) + { + return in_array($category, $this->categoryList); + } + + protected function getConnectionIdListByUserId($userId) + { + if (!isset($this->userIdConnectionIdListMap[$userId])) return []; + return $this->userIdConnectionIdListMap[$userId]; + } + + protected function getUserIdByConnection(ConnectionInterface $connection) + { + if (!isset($this->connectionIdUserIdMap[$connection->resourceId])) return; + return $this->connectionIdUserIdMap[$connection->resourceId]; + } + + protected function subscribeUser(ConnectionInterface $connection, $userId) + { + $resourceId = $connection->resourceId; + + $this->connectionIdUserIdMap[$resourceId] = $userId; + + if (!isset($this->userIdConnectionIdListMap[$userId])) $this->userIdConnectionIdListMap[$userId] = []; + + if (!in_array($resourceId, $this->userIdConnectionIdListMap[$userId])) { + $this->userIdConnectionIdListMap[$userId][] = $resourceId; + } + + $this->connections[$resourceId] = $connection; + + echo "{$userId} subscribed\n"; + } + + protected function unsubscribeUser(ConnectionInterface $connection, $userId) + { + $resourceId = $connection->resourceId; + + unset($this->connectionIdUserIdMap[$resourceId]); + + if (isset($this->userIdConnectionIdListMap[$userId])) { + $index = array_search($resourceId, $this->userIdConnectionIdListMap[$userId]); + if ($index !== false) { + $this->userIdConnectionIdListMap[$userId] = array_splice($this->userIdConnectionIdListMap[$userId], $index, 1); + } + } + + echo "{$userId} unsubscribed\n"; + } + + public function onOpen(ConnectionInterface $connection) + { + echo "onOpen {$connection->resourceId}\n"; + + $query = $connection->httpRequest->getUri()->getQuery(); + $params = \GuzzleHttp\Psr7\parse_query($query ?: ''); + if (empty($params['userId']) || empty($params['authToken'])) { + $this->closeConnection($connection); + return; + } + + $authToken = preg_replace('/[^a-zA-Z0-9]+/', '', $params['authToken']); + $userId = $params['userId']; + + $result = shell_exec("php auth_token_check.php " . $authToken); + if (empty($result)) { + $this->closeConnection($connection); + return; + } + + if ($result !== $userId) { + $this->closeConnection($connection); + return; + } + + $this->subscribeUser($connection, $userId); + } + + protected function closeConnection(ConnectionInterface $connection) + { + $userId = $this->getUserIdByConnection($connection); + if ($userId) { + $this->unsubscribeUser($connection, $userId); + } + + $connection->close(); + } + + public function onClose(ConnectionInterface $connection) + { + echo "onClose {$connection->resourceId}\n"; + + $userId = $this->getUserIdByConnection($connection); + + if ($userId) { + $this->unsubscribeUser($connection, $userId); + } + + unset($this->connections[$connection->resourceId]); + } + + public function onCall(ConnectionInterface $connection, $id, $topic, array $params) + { + $connection->callError($id, $topic, 'You are not allowed to make calls')->close(); + } + + public function onPublish(ConnectionInterface $connection, $topic, $event, array $exclude, array $eligible) + { + $topicId = $topic->getId(); + $connection->close(); + } + + public function onError(ConnectionInterface $connection, \Exception $e) + { + } + + public function onMessageReceive($dataString) + { + $data = json_decode($dataString); + + if (!property_exists($data, 'category')) return; + if (!property_exists($data, 'userId')) return; + + $userId = $data->userId; + $category = $data->category; + + if (!$userId || !$category) return; + + if (!in_array($category, $this->categoryList)) return; + + foreach ($this->getConnectionIdListByUserId($userId) as $connectionId) { + if (!isset($this->connections[$connectionId])) continue; + if (!isset($this->connectionIdTopicIdListMap[$connectionId])) continue; + + $connection = $this->connections[$connectionId]; + + if (in_array($category, $this->connectionIdTopicIdListMap[$connectionId])) { + echo "send {$category} for connection {$connectionId}\n"; + $connection->event($category, $data); + } + } + + echo "onMessage {$category} for {$userId}\n"; + } +} diff --git a/application/Espo/Core/WebSocket/Submission.php b/application/Espo/Core/WebSocket/Submission.php new file mode 100644 index 0000000000..c337c6902f --- /dev/null +++ b/application/Espo/Core/WebSocket/Submission.php @@ -0,0 +1,60 @@ +config = $config; + } + + public function submit(string $category, ?string $userId = null, $data = null) + { + if (!$data) $data = (object) []; + + $dsn = $this->config->get('webSocketSubmissionDsn', 'tcp://localhost:5555'); + + $data->userId = $userId; + $data->category = $category; + + try { + $context = new \ZMQContext(); + $socket = $context->getSocket(\ZMQ::SOCKET_PUSH, 'my pusher'); + $socket->connect($dsn); + + $socket->send(json_encode($data)); + } catch (\Throwable $e) { + $GLOBALS['log']->error("WebSocketSubmission: " . $e->getMessage()); + } + } +} diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php index a7892eba89..405c1df87f 100644 --- a/application/Espo/Core/defaults/config.php +++ b/application/Espo/Core/defaults/config.php @@ -177,5 +177,6 @@ return [ 'noteDeleteThresholdPeriod' => '1 month', 'noteEditThresholdPeriod' => '7 days', 'emailForceUseExternalClient' => false, + 'useWebSocket' => false, 'isInstalled' => false ]; diff --git a/application/Espo/Core/defaults/systemConfig.php b/application/Espo/Core/defaults/systemConfig.php index ce6bdfacea..42c6e0fa90 100644 --- a/application/Espo/Core/defaults/systemConfig.php +++ b/application/Espo/Core/defaults/systemConfig.php @@ -200,6 +200,7 @@ return [ 'cronDisabled', 'maintenanceMode', 'siteUrl', + 'useWebSocket', ], 'userItems' => [ 'outboundEmailFromAddress', diff --git a/application/Espo/Hooks/Notification/WebSocketSubmit.php b/application/Espo/Hooks/Notification/WebSocketSubmit.php new file mode 100644 index 0000000000..8abcaf1cea --- /dev/null +++ b/application/Espo/Hooks/Notification/WebSocketSubmit.php @@ -0,0 +1,52 @@ +addDependency('webSocketSubmission'); + } + + public function afterSave(Entity $entity, array $options = []) + { + if (!$this->getConfig()->get('useWebSocket')) return; + if (!$entity->isNew()) return; + $userId = $entity->get('userId'); + if (!$userId) return; + + $this->getInjection('webSocketSubmission')->submit('newNotification', $userId); + } +} diff --git a/application/Espo/Modules/Crm/Business/Reminder/EmailReminder.php b/application/Espo/Modules/Crm/Business/Reminder/EmailReminder.php index d34493f3e0..a8c57e0ce4 100644 --- a/application/Espo/Modules/Crm/Business/Reminder/EmailReminder.php +++ b/application/Espo/Modules/Crm/Business/Reminder/EmailReminder.php @@ -116,10 +116,14 @@ class EmailReminder $user = $this->getEntityManager()->getEntity('User', $reminder->get('userId')); $entity = $this->getEntityManager()->getEntity($reminder->get('entityType'), $reminder->get('entityId')); + if (!$user || !$entity) return; $emailAddress = $user->get('emailAddress'); + if (!$emailAddress) return; - if (empty($user) || empty($emailAddress) || empty($entity)) { - return; + if ($entity->hasLinkMultipleField('users')) { + $entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']); + $status = $entity->getLinkMultipleColumn('users', 'status', $user->id); + if ($status === 'Declined') return; } $email = $this->getEntityManager()->getEntity('Email'); @@ -128,9 +132,9 @@ class EmailReminder $subjectTpl = $this->getTemplateFileManager()->getTemplate('reminder', 'subject', $entity->getEntityType(), 'Crm'); $bodyTpl = $this->getTemplateFileManager()->getTemplate('reminder', 'body', $entity->getEntityType(), 'Crm'); - $subjectTpl = str_replace(array("\n", "\r"), '', $subjectTpl); + $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); - $data = array(); + $data = []; $siteUrl = rtrim($this->getConfig()->get('siteUrl'), '/'); $recordUrl = $siteUrl . '/#' . $entity->getEntityType() . '/view/' . $entity->id; @@ -164,4 +168,3 @@ class EmailReminder $emailSender->send($email); } } - diff --git a/application/Espo/Modules/Crm/Jobs/SubmitPopupReminders.php b/application/Espo/Modules/Crm/Jobs/SubmitPopupReminders.php new file mode 100644 index 0000000000..2daa2ddd1c --- /dev/null +++ b/application/Espo/Modules/Crm/Jobs/SubmitPopupReminders.php @@ -0,0 +1,122 @@ +getConfig()->get('useWebSocket')) return; + + $dt = new \DateTime(); + + $now = $dt->format('Y-m-d H:i:s'); + + $pastHours = $this->getConfig()->get('reminderPastHours', self::REMINDER_PAST_HOURS); + $nowShifted = $dt->modify('-' . $pastHours . ' hours')->format('Y-m-d H:i:s'); + + $reminderList = $this->getEntityManager()->getRepository('Reminder')->where([ + 'type' => 'Popup', + 'remindAt<=' => $now, + 'startAt>' => $nowShifted, + ])->find(); + + $submitData = []; + + foreach ($reminderList as $reminder) { + $userId = $reminder->get('userId'); + $entityType = $reminder->get('entityType'); + $entityId = $reminder->get('entityId'); + + if (!$userId || !$entityType || !$entityId) { + $this->deleteReminder($reminder); + continue; + } + + $entity = $this->getEntityManager()->getEntity($entityType, $entityId); + + if (!$entity) { + $this->deleteReminder($reminder); + continue; + } + + if ($entity->hasLinkMultipleField('users')) { + $entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']); + $status = $entity->getLinkMultipleColumn('users', 'status', $userId); + if ($status === 'Declined') { + $this->deleteReminder($reminder); + continue; + } + } + + $dateAttribute = 'dateStart'; + if ($entityType === 'Task') { + $dateAttribute = 'dateEnd'; + } + + $data = [ + 'id' => $reminder->id, + 'data' => [ + 'id' => $entity->id, + 'entityType' => $entityType, + $dateAttribute => $entity->get($dateAttribute), + 'name' => $entity->get('name'), + ], + ]; + + if (!array_key_exists($userId, $submitData)) { + $submitData[$userId] = []; + } + + $submitData[$userId][] = $data; + } + + foreach ($submitData as $userId => $list) { + try { + $this->getContainer()->get('webSocketSubmission')->submit('popupNotifications.event', $userId, (object) [ + 'list' => $list + ]); + } catch (\Throwable $e) { + $GLOBALS['log']->error('Job SubmitPopupReminders: [' . $e->getCode() . '] ' .$e->getMessage()); + } + } + + return true; + } + + protected function deleteReminder($reminder) + { + $this->getEntityManager()->getRepository('Reminder')->deleteFromDb($reminder->id); + } +} diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json index 28d220203d..90eff94815 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json @@ -2,6 +2,8 @@ "event": { "url": "Activities/action/popupNotifications", "interval": 15, + "useWebSocket": true, + "portalDisabled": true, "view": "crm:views/meeting/popup-notification" } } diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json b/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json new file mode 100644 index 0000000000..7b0eb1d978 --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/metadata/app/webSocket.json @@ -0,0 +1,5 @@ +{ + "categories": { + "popupNotifications.event": {} + } +} diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/ScheduledJob.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/ScheduledJob.json index e320268120..6edfa9c02b 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/ScheduledJob.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/ScheduledJob.json @@ -2,5 +2,12 @@ "jobSchedulingMap": { "ProcessMassEmail": "15 * * * *", "ControlKnowledgeBaseArticleStatus": "10 1 * * *" + }, + "jobs": { + "SubmitPopupReminders": { + "name": "Submit Popup Reminders", + "isSystem": true, + "scheduling": "* * * * *" + } } } diff --git a/application/Espo/Modules/Crm/Services/Activities.php b/application/Espo/Modules/Crm/Services/Activities.php index bea3f1c2f6..bf3dac2e8f 100644 --- a/application/Espo/Modules/Crm/Services/Activities.php +++ b/application/Espo/Modules/Crm/Services/Activities.php @@ -1475,7 +1475,7 @@ class Activities extends \Espo\Core\Services\Base $sth->execute(); $rowList = $sth->fetchAll(PDO::FETCH_ASSOC); - $result = array(); + $resultList = []; foreach ($rowList as $row) { $reminderId = $row['id']; $entityType = $row['entityType']; @@ -1486,7 +1486,7 @@ class Activities extends \Espo\Core\Services\Base if ($entity) { if ($entity->hasLinkMultipleField('users')) { - $entity->loadLinkMultipleField('users', array('status' => 'acceptanceStatus')); + $entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']); $status = $entity->getLinkMultipleColumn('users', 'status', $userId); if ($status === 'Declined') { $this->removeReminder($reminderId); @@ -1499,22 +1499,22 @@ class Activities extends \Espo\Core\Services\Base $dateAttribute = 'dateEnd'; } - $data = array( + $data = [ 'id' => $entity->id, 'entityType' => $entityType, $dateAttribute => $entity->get($dateAttribute), 'name' => $entity->get('name') - ); + ]; } else { continue; } - $result[] = array( + $resultList[] = [ 'id' => $reminderId, 'data' => $data - ); + ]; } - return $result; + return $resultList; } public function getUpcomingActivities($userId, $params = array(), $entityTypeList = null, $futureDays = null) diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 027369c82c..9ca71ec054 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -116,7 +116,8 @@ "daemonMaxProcessNumber": "Daemon Max Process Number", "daemonProcessTimeout": "Daemon Process Timeout", "cronDisabled": "Disable Cron", - "maintenanceMode": "Maintenance Mode" + "maintenanceMode": "Maintenance Mode", + "useWebSocket": "Use WebSocket" }, "options": { "weekStart": { diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json index 5fd89f386f..edaf79c16f 100644 --- a/application/Espo/Resources/layouts/Settings/settings.json +++ b/application/Espo/Resources/layouts/Settings/settings.json @@ -4,7 +4,8 @@ "rows": [ [{"name": "useCache"}, {"name": "siteUrl"}], [{"name": "b2cMode"}, {"name": "aclStrictMode"}], - [{"name": "maintenanceMode"}, {"name": "cronDisabled"}] + [{"name": "maintenanceMode"}, {"name": "cronDisabled"}], + [{"name": "useWebSocket"}, false] ] }, { diff --git a/application/Espo/Resources/metadata/app/webSocket.json b/application/Espo/Resources/metadata/app/webSocket.json new file mode 100644 index 0000000000..7f81831aec --- /dev/null +++ b/application/Espo/Resources/metadata/app/webSocket.json @@ -0,0 +1,5 @@ +{ + "categories": { + "newNotification": {} + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 179eb890dd..2eabd6d364 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -544,6 +544,9 @@ "maintenanceMode": { "type": "bool", "tooltip": true + }, + "useWebSocket": { + "type": "bool" } } } diff --git a/application/Espo/Services/Notification.php b/application/Espo/Services/Notification.php index 9885e5f17d..397404e675 100644 --- a/application/Espo/Services/Notification.php +++ b/application/Espo/Services/Notification.php @@ -40,6 +40,12 @@ class Notification extends \Espo\Services\Record { protected $actionHistoryDisabled = true; + protected function init() + { + parent::init(); + $this->addDependency('container'); + } + public function notifyAboutMentionInPost($userId, $noteId) { $notification = $this->getEntityManager()->getEntity('Notification'); @@ -84,6 +90,12 @@ class Notification extends \Espo\Services\Record $sql .= implode(", ", $arr); $pdo->query($sql); + + if ($this->getConfig()->get('useWebSocket')) { + foreach ($userIdList as $useId) { + $this->getInjection('container')->get('webSocketSubmission')->submit('newNotification', $userId); + } + } } public function checkUserNoteAccess(\Espo\Entities\User $user, \Espo\Entities\Note $note) diff --git a/auth_token_check.php b/auth_token_check.php new file mode 100644 index 0000000000..f79799719a --- /dev/null +++ b/auth_token_check.php @@ -0,0 +1,59 @@ +getContainer()->get('entityManager'); + +$authToken = $entityManager->getRepository('AuthToken')->where([ + 'token' => $token, + 'isActive' => true, +])->findOne(); + +if (!$authToken) exit; +if (!$authToken->get('userId')) exit; + +$userId = $authToken->get('userId'); + +$user = $entityManager->getEntity('User', $userId); +if (!$user) exit; + +ob_end_clean(); + +echo $user->id; + +exit; diff --git a/client/lib/autobahn.js b/client/lib/autobahn.js new file mode 100644 index 0000000000..7d3f547039 --- /dev/null +++ b/client/lib/autobahn.js @@ -0,0 +1,824 @@ +/** @license MIT License (c) 2011,2012 Copyright Tavendo GmbH. */ + +/** + * AutobahnJS - http://autobahn.ws + * + * A lightweight implementation of + * + * WAMP (The WebSocket Application Messaging Protocol) - http://wamp.ws + * + * Provides asynchronous RPC/PubSub over WebSocket. + * + * Copyright 2011, 2012 Tavendo GmbH. Licensed under the MIT License. + * See license text at http://www.opensource.org/licenses/mit-license.php + */ + +"use strict"; + +/** @define {string} */ +var AUTOBAHNJS_VERSION = '?.?.?'; + +/** @define {boolean} */ +var AUTOBAHNJS_DEBUG = true; + + + +var ab = window.ab = {}; + +ab._version = AUTOBAHNJS_VERSION; + +/** + * Fallbacks for browsers lacking + * + * Array.prototype.indexOf + * Array.prototype.forEach + * + * most notably MSIE8. + * + * Source: + * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf + * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach + */ +(function () { + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + "use strict"; + if (this === null) { + throw new TypeError(); + } + var t = new Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n !== 0 && n !== Infinity && n !== -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + if (!Array.prototype.forEach) { + + Array.prototype.forEach = function (callback, thisArg) { + + var T, k; + + if (this === null) { + throw new TypeError(" this is null or not defined"); + } + + // 1. Let O be the result of calling ToObject passing the |this| value as the argument. + var O = new Object(this); + + // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; // Hack to convert O.length to a UInt32 + + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if ({}.toString.call(callback) !== "[object Function]") { + throw new TypeError(callback + " is not a function"); + } + + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (thisArg) { + T = thisArg; + } + + // 6. Let k be 0 + k = 0; + + // 7. Repeat, while k < len + while (k < len) { + + var kValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + + // i. Let kValue be the result of calling the Get internal method of O with argument Pk. + kValue = O[k]; + + // ii. Call the Call internal method of callback with T as the this value and + // argument list containing kValue, k, and O. + callback.call(T, kValue, k, O); + } + // d. Increase k by 1. + k++; + } + // 8. return undefined + }; + } + +})(); + + +// Helper to slice out browser / version from userAgent +ab._sliceUserAgent = function (str, delim, delim2) { + var ver = []; + var ua = navigator.userAgent; + var i = ua.indexOf(str); + var j = ua.indexOf(delim, i); + if (j < 0) { + j = ua.length; + } + var agent = ua.slice(i, j).split(delim2); + var v = agent[1].split('.'); + for (var k = 0; k < v.length; ++k) { + ver.push(parseInt(v[k], 10)); + } + return {name: agent[0], version: ver}; +}; + +/** + * Detect browser and browser version. + */ +ab.getBrowser = function () { + + var ua = navigator.userAgent; + if (ua.indexOf("Chrome") > -1) { + return ab._sliceUserAgent("Chrome", " ", "/"); + } else if (ua.indexOf("Safari") > -1) { + return ab._sliceUserAgent("Safari", " ", "/"); + } else if (ua.indexOf("Firefox") > -1) { + return ab._sliceUserAgent("Firefox", " ", "/"); + } else if (ua.indexOf("MSIE") > -1) { + return ab._sliceUserAgent("MSIE", ";", " "); + } else { + return null; + } +}; + + +// Logging message for unsupported browser. +ab.browserNotSupportedMessage = "Browser does not support WebSockets (RFC6455)"; + + +ab._idchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +ab._idlen = 16; +ab._subprotocol = "wamp"; + +ab._newid = function () { + var id = ""; + for (var i = 0; i < ab._idlen; i += 1) { + id += ab._idchars.charAt(Math.floor(Math.random() * ab._idchars.length)); + } + return id; +}; + +ab.log = function (o) { + if (window.console && console.log) { + //console.log.apply(console, !!arguments.length ? arguments : [this]); + if (arguments.length > 1) { + console.group("Log Item"); + for (var i = 0; i < arguments.length; i += 1) { + console.log(arguments[i]); + } + console.groupEnd(); + } else { + console.log(arguments[0]); + } + } +}; + +ab._debugrpc = false; +ab._debugpubsub = false; +ab._debugws = false; + +ab.debug = function (debugWamp, debugWs) { + if ("console" in window) { + ab._debugrpc = debugWamp; + ab._debugpubsub = debugWamp; + ab._debugws = debugWs; + } else { + throw "browser does not support console object"; + } +}; + +ab.version = function () { + return ab._version; +}; + +ab.PrefixMap = function () { + + var self = this; + self._index = {}; + self._rindex = {}; +}; + +ab.PrefixMap.prototype.get = function (prefix) { + + var self = this; + return self._index[prefix]; +}; + +ab.PrefixMap.prototype.set = function (prefix, uri) { + + var self = this; + self._index[prefix] = uri; + self._rindex[uri] = prefix; +}; + +ab.PrefixMap.prototype.setDefault = function (uri) { + + var self = this; + self._index[""] = uri; + self._rindex[uri] = ""; +}; + +ab.PrefixMap.prototype.remove = function (prefix) { + + var self = this; + var uri = self._index[prefix]; + if (uri) { + delete self._index[prefix]; + delete self._rindex[uri]; + } +}; + +ab.PrefixMap.prototype.resolve = function (curie, pass) { + + var self = this; + + // skip if not a CURIE + var i = curie.indexOf(":"); + if (i >= 0) { + var prefix = curie.substring(0, i); + if (self._index[prefix]) { + return self._index[prefix] + curie.substring(i + 1); + } + } + + // either pass-through or null + if (pass == true) { + return curie; + } else { + return null; + } +}; + +ab.PrefixMap.prototype.shrink = function (uri, pass) { + + var self = this; + + // skip if already a CURIE + var i = uri.indexOf(":"); + if (i == -1) { + for (var i = uri.length; i > 0; i -= 1) { + var u = uri.substring(0, i); + var p = self._rindex[u]; + if (p) { + return p + ":" + uri.substring(i); + } + } + } + + // either pass-through or null + if (pass == true) { + return uri; + } else { + return null; + } +}; + + +ab._MESSAGE_TYPEID_WELCOME = 0; +ab._MESSAGE_TYPEID_PREFIX = 1; +ab._MESSAGE_TYPEID_CALL = 2; +ab._MESSAGE_TYPEID_CALL_RESULT = 3; +ab._MESSAGE_TYPEID_CALL_ERROR = 4; +ab._MESSAGE_TYPEID_SUBSCRIBE = 5; +ab._MESSAGE_TYPEID_UNSUBSCRIBE = 6; +ab._MESSAGE_TYPEID_PUBLISH = 7; +ab._MESSAGE_TYPEID_EVENT = 8; + + +ab.CONNECTION_CLOSED = 0; +ab.CONNECTION_LOST = 1; +ab.CONNECTION_UNREACHABLE = 2; +ab.CONNECTION_UNSUPPORTED = 3; + +ab.Session = function (wsuri, onopen, onclose, options) { + + var self = this; + + self._wsuri = wsuri; + self._options = options; + self._websocket_onopen = onopen; + self._websocket_onclose = onclose; + self._websocket = null; + self._websocket_connected = false; + self._session_id = null; + self._calls = {}; + self._subscriptions = {}; + self._prefixes = new ab.PrefixMap(); + + self._txcnt = 0; + self._rxcnt = 0; + + if ("WebSocket" in window) { + // Chrome, MSIE, newer Firefox + self._websocket = new WebSocket(self._wsuri, [ab._subprotocol]); + } else if ("MozWebSocket" in window) { + // older versions of Firefox prefix the WebSocket object + self._websocket = new MozWebSocket(self._wsuri, [ab._subprotocol]); + } else { + if (onclose !== undefined) { + onclose(ab.CONNECTION_UNSUPPORTED); + return; + } else { + throw ab.browserNotSupportedMessage; + } + } + + self._websocket.onmessage = function (e) + { + if (ab._debugws) { + self._rxcnt += 1; + console.group("WS Receive"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(self._rxcnt); + console.log(e.data); + console.groupEnd(); + } + + var o = JSON.parse(e.data); + if (o[1] in self._calls) + { + if (o[0] === ab._MESSAGE_TYPEID_CALL_RESULT) { + + var dr = self._calls[o[1]]; + var r = o[2]; + + if (ab._debugrpc && dr._ab_callobj !== undefined) { + console.group("WAMP Call", dr._ab_callobj[2]); + console.timeEnd(dr._ab_tid); + console.group("Arguments"); + for (var i = 3; i < dr._ab_callobj.length; i += 1) { + var arg = dr._ab_callobj[i]; + if (arg !== undefined) { + console.log(arg); + } else { + break; + } + } + console.groupEnd(); + console.group("Result"); + console.log(r); + console.groupEnd(); + console.groupEnd(); + } + + dr.resolve(r); + } + else if (o[0] === ab._MESSAGE_TYPEID_CALL_ERROR) { + + var de = self._calls[o[1]]; + var uri = o[2]; + var desc = o[3]; + var detail = o[4]; + + if (ab._debugrpc && de._ab_callobj !== undefined) { + console.group("WAMP Call", de._ab_callobj[2]); + console.timeEnd(de._ab_tid); + console.group("Arguments"); + for (var j = 3; j < de._ab_callobj.length; j += 1) { + var arg2 = de._ab_callobj[j]; + if (arg2 !== undefined) { + console.log(arg2); + } else { + break; + } + } + console.groupEnd(); + console.group("Error"); + console.log(uri); + console.log(desc); + if (detail !== undefined) { + console.log(detail); + } + console.groupEnd(); + console.groupEnd(); + } + + if (detail !== undefined) { + de.reject(uri, desc, detail); + } else { + de.reject(uri, desc); + } + } + delete self._calls[o[1]]; + } + else if (o[0] === ab._MESSAGE_TYPEID_EVENT) + { + var subid = self._prefixes.resolve(o[1], true); + if (subid in self._subscriptions) { + + var uri2 = o[1]; + var val = o[2]; + + if (ab._debugpubsub) { + console.group("WAMP Event"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(uri2); + console.log(val); + console.groupEnd(); + } + + self._subscriptions[subid].forEach(function (callback) { + + callback(uri2, val); + }); + } + else { + // ignore unsolicited event! + } + } + else if (o[0] === ab._MESSAGE_TYPEID_WELCOME) + { + if (self._session_id === null) { + self._session_id = o[1]; + self._wamp_version = o[2]; + self._server = o[3]; + + if (ab._debugrpc || ab._debugpubsub) { + console.group("WAMP Welcome"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(self._wamp_version); + console.log(self._server); + console.groupEnd(); + } + + // only now that we have received the initial server-to-client + // welcome message, fire application onopen() hook + if (self._websocket_onopen !== null) { + self._websocket_onopen(self._session_id, self._wamp_version, self._server); + } + } else { + throw "protocol error (welcome message received more than once)"; + } + } + }; + + self._websocket.onopen = function (e) + { + // check if we can speak WAMP! + if (self._websocket.protocol !== ab._subprotocol) { + + if (typeof self._websocket.protocol === 'undefined') { + // i.e. Safari does subprotocol negotiation (broken), but then + // does NOT set the protocol attribute of the websocket object (broken) + // + if (ab._debugws) { + console.group("WS Warning"); + console.info(self._wsuri); + console.log("WebSocket object has no protocol attribute: WAMP subprotocol check skipped!"); + console.groupEnd(); + } + } + else if (self._options && self._options.skipSubprotocolCheck) { + // WAMP subprotocol check disabled by session option + // + if (ab._debugws) { + console.group("WS Warning"); + console.info(self._wsuri); + console.log("Server does not speak WAMP, but subprotocol check disabled by option!"); + console.log(self._websocket.protocol); + console.groupEnd(); + } + } else { + // we only speak WAMP .. if the server denied us this, we bail out. + // + self._websocket.close(1000, "server does not speak WAMP"); + throw "server does not speak WAMP (but '" + self._websocket.protocol + "' !)"; + } + } + if (ab._debugws) { + console.group("WAMP Connect"); + console.info(self._wsuri); + console.log(self._websocket.protocol); + console.groupEnd(); + } + self._websocket_connected = true; + }; + + self._websocket.onerror = function (e) + { + // FF fires this upon unclean closes + // Chrome does not fire this + }; + + self._websocket.onclose = function (e) + { + if (ab._debugws) { + if (self._websocket_connected) { + console.log("Autobahn connection to " + self._wsuri + " lost (code " + e.code + ", reason '" + e.reason + "', wasClean " + e.wasClean + ")."); + } else { + console.log("Autobahn could not connect to " + self._wsuri + " (code " + e.code + ", reason '" + e.reason + "', wasClean " + e.wasClean + ")."); + } + } + + // fire app callback + if (self._websocket_onclose !== undefined) { + if (self._websocket_connected) { + if (e.wasClean) { + // connection was closed cleanly (closing HS was performed) + self._websocket_onclose(ab.CONNECTION_CLOSED); + } else { + // connection was closed uncleanly (lost without closing HS) + self._websocket_onclose(ab.CONNECTION_LOST); + } + } else { + // connection could not be established in the first place + self._websocket_onclose(ab.CONNECTION_UNREACHABLE); + } + } + + // cleanup - reconnect requires a new session object! + self._websocket_connected = false; + self._wsuri = null; + self._websocket_onopen = null; + self._websocket_onclose = null; + self._websocket = null; + }; +}; + + +ab.Session.prototype._send = function (msg) { + + var self = this; + + if (!self._websocket_connected) { + throw "Autobahn not connected"; + } + + var rmsg = JSON.stringify(msg); + self._websocket.send(rmsg); + self._txcnt += 1; + + if (ab._debugws) { + console.group("WS Send"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(self._txcnt); + console.log(rmsg); + console.groupEnd(); + } +}; + + +ab.Session.prototype.close = function () { + + var self = this; + + if (!self._websocket_connected) { + throw "Autobahn not connected"; + } + + self._websocket.close(); +}; + + +ab.Session.prototype.sessionid = function () { + + var self = this; + return self._session_id; +}; + + +ab.Session.prototype.shrink = function (uri, pass) { + + var self = this; + return self._prefixes.shrink(uri, pass); +}; + + +ab.Session.prototype.resolve = function (curie, pass) { + + var self = this; + return self._prefixes.resolve(curie, pass); +}; + + +ab.Session.prototype.prefix = function (prefix, uri) { + + var self = this; + + if (self._prefixes.get(prefix) !== undefined) { + throw "prefix '" + prefix + "' already defined"; + } + + self._prefixes.set(prefix, uri); + + if (ab._debugrpc || ab._debugpubsub) { + console.group("WAMP Prefix"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(prefix); + console.log(uri); + console.groupEnd(); + } + + var msg = [ab._MESSAGE_TYPEID_PREFIX, prefix, uri]; + self._send(msg); +}; + + +ab.Session.prototype.call = function () { + + var self = this; + + var d = new when.defer(); + var callid; + while (true) { + callid = ab._newid(); + if (!(callid in self._calls)) { + break; + } + } + self._calls[callid] = d; + + var procuri = self._prefixes.shrink(arguments[0], true); + var obj = [ab._MESSAGE_TYPEID_CALL, callid, procuri]; + for (var i = 1; i < arguments.length; i += 1) { + obj.push(arguments[i]); + } + + self._send(obj); + + if (ab._debugrpc) { + d._ab_callobj = obj; + d._ab_tid = self._wsuri + " [" + self._session_id + "][" + callid + "]"; + console.time(d._ab_tid); + console.info(); + } + + return d; +}; + + +ab.Session.prototype.subscribe = function (topicuri, callback) { + + var self = this; + + // subscribe by sending WAMP message when topic not already subscribed + // + var rtopicuri = self._prefixes.resolve(topicuri, true); + if (!(rtopicuri in self._subscriptions)) { + + if (ab._debugpubsub) { + console.group("WAMP Subscribe"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(topicuri); + console.log(callback); + console.groupEnd(); + } + + var msg = [ab._MESSAGE_TYPEID_SUBSCRIBE, topicuri]; + self._send(msg); + + self._subscriptions[rtopicuri] = []; + } + + // add callback to event listeners list if not already in list + // + var i = self._subscriptions[rtopicuri].indexOf(callback); + if (i === -1) { + self._subscriptions[rtopicuri].push(callback); + } + else { + throw "callback " + callback + " already subscribed for topic " + rtopicuri; + } +}; + + +ab.Session.prototype.unsubscribe = function (topicuri, callback) { + + var self = this; + + var rtopicuri = self._prefixes.resolve(topicuri, true); + if (!(rtopicuri in self._subscriptions)) { + throw "not subscribed to topic " + rtopicuri; + } + else { + var removed; + if (callback !== undefined) { + var idx = self._subscriptions[rtopicuri].indexOf(callback); + if (idx !== -1) { + removed = callback; + self._subscriptions[rtopicuri].splice(idx, 1); + } + else { + throw "no callback " + callback + " subscribed on topic " + rtopicuri; + } + } + else { + removed = self._subscriptions[rtopicuri].slice(); + self._subscriptions[rtopicuri] = []; + } + + if (self._subscriptions[rtopicuri].length === 0) { + + delete self._subscriptions[rtopicuri]; + + if (ab._debugpubsub) { + console.group("WAMP Unsubscribe"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(topicuri); + console.log(removed); + console.groupEnd(); + } + + var msg = [ab._MESSAGE_TYPEID_UNSUBSCRIBE, topicuri]; + self._send(msg); + } + } +}; + + +ab.Session.prototype.publish = function () { + + var self = this; + + var topicuri = arguments[0]; + var event = arguments[1]; + + var excludeMe = null; + var exclude = null; + var eligible = null; + + var msg = null; + + if (arguments.length > 3) { + + if (!(arguments[2] instanceof Array)) { + throw "invalid argument type(s)"; + } + if (!(arguments[3] instanceof Array)) { + throw "invalid argument type(s)"; + } + + exclude = arguments[2]; + eligible = arguments[3]; + msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, exclude, eligible]; + + } else if (arguments.length > 2) { + + if (typeof(arguments[2]) === 'boolean') { + + excludeMe = arguments[2]; + msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, excludeMe]; + + } else if (arguments[2] instanceof Array) { + + exclude = arguments[2]; + msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, exclude]; + + } else { + throw "invalid argument type(s)"; + } + + } else { + + msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event]; + } + + if (ab._debugpubsub) { + console.group("WAMP Publish"); + console.info(self._wsuri + " [" + self._session_id + "]"); + console.log(topicuri); + console.log(event); + + if (excludeMe !== null) { + console.log(excludeMe); + } else { + if (exclude !== null) { + console.log(exclude); + if (eligible !== null) { + console.log(eligible); + } + } + } + console.groupEnd(); + } + + self._send(msg); +}; \ No newline at end of file diff --git a/client/modules/crm/src/views/meeting/popup-notification.js b/client/modules/crm/src/views/meeting/popup-notification.js index e93d77ec23..b9bb44cf94 100644 --- a/client/modules/crm/src/views/meeting/popup-notification.js +++ b/client/modules/crm/src/views/meeting/popup-notification.js @@ -87,4 +87,3 @@ Espo.define('crm:views/meeting/popup-notification', 'views/popup-notification', }); }); - diff --git a/client/src/app.js b/client/src/app.js index 640c64ad9b..99aa1083c2 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -50,7 +50,8 @@ Espo.define( 'layout-manager', 'theme-manager', 'session-storage', - 'view-helper' + 'view-helper', + 'web-socket-manager' ], function ( Ui, @@ -73,7 +74,8 @@ Espo.define( LayoutManager, ThemeManager, SessionStorage, - ViewHelper + ViewHelper, + WebSocketManager ) { var App = function (options, callback) { @@ -141,6 +143,10 @@ Espo.define( this.modelFactory = new ModelFactory(this.loader, this.metadata, this.user); this.collectionFactory = new CollectionFactory(this.loader, this.modelFactory); + if (this.settings.get('useWebSocket')) { + this.webSocketManager = new WebSocketManager(this.settings); + } + this.initDateTime(); this.initView(); this.initBaseController(); @@ -223,6 +229,10 @@ Espo.define( this.loadStylesheet(); } + if (this.webSocketManager) { + this.webSocketManager.connect(this.auth, this.user.id); + } + var promiseList = []; var aclImplementationClassMap = {}; @@ -396,6 +406,7 @@ Espo.define( helper.sessionStorage = this.sessionStorage; helper.basePath = this.basePath; helper.appParams = this.appParams; + helper.webSocketManager = this.webSocketManager; this.viewLoader = function (viewName, callback) { Espo.require(Espo.Utils.composeViewClassName(viewName), callback); @@ -493,6 +504,10 @@ Espo.define( } } + if (this.webSocketManager) { + this.webSocketManager.close(); + } + this.auth = null; this.user.clear(); this.preferences.clear(); diff --git a/client/src/views/admin/settings.js b/client/src/views/admin/settings.js index 026db37dab..ae770d6795 100644 --- a/client/src/views/admin/settings.js +++ b/client/src/views/admin/settings.js @@ -38,6 +38,7 @@ Espo.define('views/admin/settings', 'views/settings/record/edit', function (Dep) if (this.getHelper().getAppParam('isRestrictedMode') && !this.getUser().isSuperAdmin()) { this.hideField('cronDisabled'); this.hideField('maintenanceMode'); + this.hideField('useWebSocket'); this.setFieldReadOnly('siteUrl'); } diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js index 0a45147e16..98ff98260b 100644 --- a/client/src/views/notification/badge.js +++ b/client/src/views/notification/badge.js @@ -54,6 +54,8 @@ Espo.define('views/notification/badge', 'view', function (Dep) { this.notificationSoundsDisabled = this.getConfig().get('notificationSoundsDisabled'); + this.useWebSocket = this.getConfig().get('useWebSocket'); + this.once('remove', function () { if (this.timeout) { clearTimeout(this.timeout); @@ -65,7 +67,6 @@ Espo.define('views/notification/badge', 'view', function (Dep) { this.notificationsCheckInterval = this.getConfig().get('notificationsCheckInterval') || this.notificationsCheckInterval; - this.popupCheckIteration = 0; this.lastId = 0; this.shownNotificationIds = []; this.closedNotificationIds = []; @@ -143,9 +144,8 @@ Espo.define('views/notification/badge', 'view', function (Dep) { return; } - $.ajax('Notification/action/notReadCount').done(function (count) { + Espo.Ajax.getRequest('Notification/action/notReadCount').done(function (count) { if (!isFirstCheck && count > this.unreadCount) { - var blockPlayNotificationSound = localStorage.getItem('blockPlayNotificationSound'); if (!blockPlayNotificationSound) { this.playSound(); @@ -167,32 +167,50 @@ Espo.define('views/notification/badge', 'view', function (Dep) { runCheckUpdates: function (isFirstCheck) { this.checkUpdates(isFirstCheck); + if (this.useWebSocket) { + this.getHelper().webSocketManager.subscribe('newNotification', function (a, b) { + console.log(a, b); + this.checkUpdates(); + }.bind(this)) + return; + } this.timeout = setTimeout(function () { this.runCheckUpdates(); }.bind(this), this.notificationsCheckInterval * 1000); }, - checkPopupNotifications: function (name) { + checkPopupNotifications: function (name, isNotFirstCheck) { var data = this.popupNotificationsData[name] || {}; var url = data.url; var interval = data.interval; var disabled = data.disabled || false; - if (disabled || !url || !interval) return; + if (disabled) return; + if (data.portalDisabled && this.getUser().isPortal()) return; - var isFirstCheck = false; - if (this.popupCheckIteration == 0) { - isFirstCheck = true; + var useWebSocket = this.useWebSocket && data.useWebSocket; + if (useWebSocket) { + var category = 'popupNotifications.' + (data.webSocketCategory || name); + this.getHelper().webSocketManager.subscribe(category, function (c, response) { + if (!response.list) return; + response.list.forEach(function (item) { + console.log(item); + this.showPopupNotification(name, item); + }, this); + }.bind(this)) } + if (!url || !interval) return; + (new Promise(function (resolve) { if (this.checkBypass()) { resolve(); return; } - var jqxhr = $.ajax(url).done(function (list) { - list.forEach(function (d) { - this.showPopupNotification(name, d, isFirstCheck); + var jqxhr = Espo.Ajax.getRequest(url).done(function (list) { + list.forEach(function (item) { + console.log(item); + this.showPopupNotification(name, item, isNotFirstCheck); }, this); }.bind(this)); @@ -200,14 +218,15 @@ Espo.define('views/notification/badge', 'view', function (Dep) { resolve(); }); }.bind(this))).then(function () { + if (useWebSocket) return; + this.popoupTimeouts[name] = setTimeout(function () { - this.popupCheckIteration++; - this.checkPopupNotifications(name); + this.checkPopupNotifications(name, isNotFirstCheck); }.bind(this), interval * 1000); }.bind(this)); }, - showPopupNotification: function (name, data, isFirstCheck) { + showPopupNotification: function (name, data, isNotFirstCheck) { var view = this.popupNotificationsData[name].view; if (!view) return; @@ -233,7 +252,7 @@ Espo.define('views/notification/badge', 'view', function (Dep) { notificationData: data.data || {}, notificationId: data.id, id: id, - isFirstCheck: isFirstCheck + isFirstCheck: !isNotFirstCheck, }, function (view) { view.render(); this.$popupContainer.removeClass('hidden'); diff --git a/client/src/views/popup-notification.js b/client/src/views/popup-notification.js index 6f67d05e99..376aa8d129 100644 --- a/client/src/views/popup-notification.js +++ b/client/src/views/popup-notification.js @@ -126,7 +126,5 @@ Espo.define('views/popup-notification', 'view', function (Dep) { this.trigger('cancel'); this.remove(); } - }); }); - diff --git a/client/src/web-socket-manager.js b/client/src/web-socket-manager.js new file mode 100644 index 0000000000..95111e8dc1 --- /dev/null +++ b/client/src/web-socket-manager.js @@ -0,0 +1,126 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +define('web-socket-manager', ['lib!client/lib/autobahn.js'], function () { + + var WebSocketManager = function (config) { + this.config = config; + var url = this.config.get('webSocketUrl'); + this.port = 8080; + + if (url) { + if (url.indexOf('wss://') === 0) { + this.url = url.substr(6); + this.protocolPart = 'wss://'; + } else { + this.url = url.substr(5); + this.protocolPart = 'ws://'; + } + if (~this.url.indexOf(':')) { + this.port = parseInt(this.url.split(':')[1]); + } + } else { + var siteUrl = this.config.get('siteUrl') || ''; + if (siteUrl.indexOf('https://') === 0) { + this.url = siteUrl.substr(8); + this.protocolPart = 'wss://'; + } else { + this.url = siteUrl.substr(7); + this.protocolPart = 'ws://'; + } + + if (~this.url.indexOf('/')) { + this.url = this.url.substr(0, this.url.indexOf('/')); + } + } + + if (~this.url.indexOf(':')) { + this.url = this.url.substr(0, this.url.indexOf(':')); + } + }; + + _.extend(WebSocketManager.prototype, { + + connect: function (auth, userId) { + try { + var authArray = Base64.decode(auth).split(':'); + var username = authArray[0]; + var authToken = authArray[1]; + var url = this.protocolPart + this.url + ':' + this.port; + + url += '?authToken=' + authToken + '&userId=' + userId; + + var connection = this.connection = new ab.Session(url, + function () {}, + function () {}, + {'skipSubprotocolCheck': true} + ); + } catch (e) { + console.error(e.message); + this.connection = null; + } + }, + + subscribe: function (category, callback) { + if (!this.connection) return; + try { + this.connection.subscribe(category, callback); + } catch (e) { + if (e.message) { + console.error(e.message); + } else { + console.error("Coud not subscribe "+category+" to WebSocket."); + } + } + }, + + unsubscribe: function (category, callback) { + if (!this.connection) return; + try { + this.connection.unsubscribe(category, callback); + } catch (e) { + if (e.message) { + console.error(e.message); + } else { + console.error("Coud not unsubscribe "+category+" from WebSocket."); + } + } + }, + + close: function () { + if (!this.connection) return; + try { + this.connection.close(); + } catch (e) { + console.error(e.message); + } + }, + }); + + return WebSocketManager; +}); diff --git a/composer.json b/composer.json index 3ee3f452a7..cae5443071 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,9 @@ "phpoffice/phpexcel": "^1.8", "phpoffice/phpspreadsheet": "^1.1", "spatie/async": "dev-for-espocrm", - "symfony/process": "4.1.7" + "symfony/process": "4.1.7", + "cboden/ratchet": "^0.4.1", + "react/zmq": "^0.4.0" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index 91f4129d2a..17561f53fc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e0c7c9338da2f7fd2ba01e84154984e4", - "content-hash": "4012989bfe64b06071fdaef6540f99ad", + "hash": "4a490462a340775bd04c25c154f661ec", + "content-hash": "8fa4566afa49ca25e6609e7789221e60", "packages": [ + { + "name": "cboden/ratchet", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Ratchet.git", + "reference": "0d31f3a8ad4795fd48397712709e55cd07f51360" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/0d31f3a8ad4795fd48397712709e55cd07f51360", + "reference": "0d31f3a8ad4795fd48397712709e55cd07f51360", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.0", + "php": ">=5.4.2", + "ratchet/rfc6455": "^0.2", + "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", + "symfony/http-foundation": "^2.6|^3.0|^4.0", + "symfony/routing": "^2.6|^3.0|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\": "src/Ratchet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + } + ], + "description": "PHP WebSocket library", + "homepage": "http://socketo.me", + "keywords": [ + "Ratchet", + "WebSockets", + "server", + "sockets", + "websocket" + ], + "time": "2017-12-12 00:49:31" + }, { "name": "composer/semver", "version": "1.4.0", @@ -567,6 +620,116 @@ ], "time": "2013-01-12 18:59:04" }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "time": "2017-07-23 21:35:13" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04 20:46:45" + }, { "name": "monolog/monolog", "version": "1.20.0", @@ -973,6 +1136,56 @@ ], "time": "2018-01-28 12:37:15" }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, { "name": "psr/log", "version": "1.0.0", @@ -1059,6 +1272,454 @@ ], "time": "2017-01-02 13:31:39" }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11 07:05:27" + }, + { + "name": "ratchet/rfc6455", + "version": "0.2.4", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "1612f528c3496ad06e910d0f8b6f16ab97696706" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/1612f528c3496ad06e910d0f8b6f16ab97696706", + "reference": "1612f528c3496ad06e910d0f8b6f16ab97696706", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.0", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "react/http": "^0.4.1", + "react/socket-client": "^0.4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "time": "2018-05-02 14:52:00" + }, + { + "name": "react/cache", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "7d7da7fb7574d471904ba357b39bbf110ccdbf66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/7d7da7fb7574d471904ba357b39bbf110ccdbf66", + "reference": "7d7da7fb7574d471904ba357b39bbf110ccdbf66", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "~2.0|~1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "time": "2018-06-25 12:52:40" + }, + { + "name": "react/dns", + "version": "v0.4.16", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "0a0bedfec72b38406413c6ea01e1c015bd0bf72b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/0a0bedfec72b38406413c6ea01e1c015bd0bf72b", + "reference": "0a0bedfec72b38406413c6ea01e1c015bd0bf72b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^0.5 || ^0.4 || ^0.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.1 || ^1.2.1", + "react/promise-timer": "^1.2", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "time": "2018-11-11 11:21:13" + }, + { + "name": "react/event-loop", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "0266aff7aa7b0613b1f38a723e14a0ebc55cfca3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/0266aff7aa7b0613b1f38a723e14a0ebc55cfca3", + "reference": "0266aff7aa7b0613b1f38a723e14a0ebc55cfca3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ^5.7 || ^6.4" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "time": "2018-07-11 14:37:46" + }, + { + "name": "react/promise", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "time": "2019-01-07 21:25:54" + }, + { + "name": "react/promise-timer", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "a11206938ca2394dc7bb368f5da25cd4533fa603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/a11206938ca2394dc7bb368f5da25cd4533fa603", + "reference": "a11206938ca2394dc7bb368f5da25cd4533fa603", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Timer\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "time": "2018-06-13 16:45:37" + }, + { + "name": "react/socket", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23b7372bb25cea934f6124f5bdac34e30161959e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23b7372bb25cea934f6124f5bdac34e30161959e", + "reference": "23b7372bb25cea934f6124f5bdac34e30161959e", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^0.4.13", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0", + "react/stream": "^1.1" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "time": "2019-01-07 14:10:13" + }, + { + "name": "react/stream", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/50426855f7a77ddf43b9266c22320df5bf6c6ce6", + "reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "time": "2019-01-01 16:15:09" + }, + { + "name": "react/zmq", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/friends-of-reactphp/zmq.git", + "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/friends-of-reactphp/zmq/zipball/13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", + "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "ext-zmq": "*", + "php": ">=5.4.0", + "react/event-loop": "^1.0 || ^0.5 || ^0.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ZMQ\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ZeroMQ bindings for React.", + "keywords": [ + "zeromq", + "zmq" + ], + "time": "2018-05-18 15:27:55" + }, { "name": "slim/slim", "version": "2.6.2", @@ -1159,6 +1820,60 @@ ], "time": "2018-12-11 08:49:14" }, + { + "name": "symfony/http-foundation", + "version": "v4.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "a633d422a09242064ba24e44a6e1494c5126de86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a633d422a09242064ba24e44a6e1494c5126de86", + "reference": "a633d422a09242064ba24e44a6e1494c5126de86", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/expression-language": "~3.4|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2019-01-05 16:37:49" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.10.0", @@ -1267,6 +1982,81 @@ "homepage": "https://symfony.com", "time": "2018-10-14 20:48:13" }, + { + "name": "symfony/routing", + "version": "v3.2.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "b382d7c4f443372c118efcd0cd2bf1028434f2f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/b382d7c4f443372c118efcd0cd2bf1028434f2f5", + "reference": "b382d7c4f443372c118efcd0cd2bf1028434f2f5", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.2", + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/http-foundation": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/dependency-injection": "For loading routes from a service", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2017-06-23 06:35:45" + }, { "name": "symfony/yaml", "version": "v2.7.0", diff --git a/websocket.php b/websocket.php new file mode 100644 index 0000000000..1fcfb38f00 --- /dev/null +++ b/websocket.php @@ -0,0 +1,55 @@ +getContainer()->get('metadata')->get(['app', 'webSocket', 'categories'], [])); + +$loop = \React\EventLoop\Factory::create(); +$pusher = new \Espo\Core\WebSocket\Pusher($categoryList); + +$context = new \React\ZMQ\Context($loop); +$pull = $context->getSocket(\ZMQ::SOCKET_PULL); +$pull->bind('tcp://127.0.0.1:5555'); +$pull->on('message', [$pusher, 'onMessageReceive']); + +$webSocket = new \React\Socket\Server('0.0.0.0:8080', $loop); +$webServer = new \Ratchet\Server\IoServer( + new \Ratchet\Http\HttpServer( + new \Ratchet\WebSocket\WsServer( + new \Ratchet\Wamp\WampServer($pusher) + ) + ), + $webSocket +); + +$loop->run();