1;(function () { 2 /** 3 * Bitrix Push & Pull 4 * Pull client 5 * 6 * @package bitrix 7 * @subpackage pull 8 * @copyright 2001-2019 Bitrix 9 */ 10 11 /****************** ATTENTION ******************************* 12 * Please do not use Bitrix CoreJS in this class. 13 * This class can be called on a page without Bitrix Framework 14 *************************************************************/ 15 16 if (!window.BX) 17 { 18 window.BX = {}; 19 } 20 else if (window.BX.PullClient) 21 { 22 return; 23 } 24 else if (!window.BX.RestClient) 25 { 26 return; 27 } 28 29 const BX = window.BX; 30 const protobuf = window.protobuf; 31 32 const REVISION = 19; // api revision - check module/pull/include.php 33 const LONG_POLLING_TIMEOUT = 60; 34 const RESTORE_WEBSOCKET_TIMEOUT = 30 * 60; 35 const CONFIG_TTL = 24 * 60 * 60; 36 const CONFIG_CHECK_INTERVAL = 60000; 37 const MAX_IDS_TO_STORE = 10; 38 const OFFLINE_STATUS_DELAY = 5000; 39 40 const LS_SESSION = "bx-pull-session"; 41 const LS_SESSION_CACHE_TIME = 20; 42 43 const ConnectionType = { 44 WebSocket: 'webSocket', 45 LongPolling: 'longPolling' 46 }; 47 48 const PullStatus = { 49 Online: 'online', 50 Offline: 'offline', 51 Connecting: 'connect' 52 }; 53 54 const SenderType = { 55 Unknown: 0, 56 Client: 1, 57 Backend: 2 58 }; 59 60 const SubscriptionType = { 61 Server: 'server', 62 Client: 'client', 63 Online: 'online', 64 Status: 'status', 65 Revision: 'revision' 66 }; 67 68 const CloseReasons = { 69 NORMAL_CLOSURE: 1000, 70 SERVER_DIE: 1001, 71 CONFIG_REPLACED: 3000, 72 CHANNEL_EXPIRED: 3001, 73 SERVER_RESTARTED: 3002, 74 CONFIG_EXPIRED: 3003, 75 MANUAL: 3004, 76 STUCK: 3005, 77 WRONG_CHANNEL_ID: 4010, 78 }; 79 80 const SystemCommands = { 81 CHANNEL_EXPIRE: 'CHANNEL_EXPIRE', 82 CONFIG_EXPIRE: 'CONFIG_EXPIRE', 83 SERVER_RESTART: 'SERVER_RESTART' 84 }; 85 86 const ServerMode = { 87 Shared: 'shared', 88 Personal: 'personal' 89 }; 90 91 const EmptyConfig = { 92 api: {}, 93 channels: {}, 94 publicChannels: {}, 95 server: {timeShift: 0}, 96 clientId: null, 97 jwt: null, 98 exp: 0, 99 }; 100 101 // Protobuf message models 102 const Response = protobuf.roots['push-server']['Response']; 103 const ResponseBatch = protobuf.roots['push-server']['ResponseBatch']; 104 const Request = protobuf.roots['push-server']['Request']; 105 const RequestBatch = protobuf.roots['push-server']['RequestBatch']; 106 const IncomingMessagesRequest = protobuf.roots['push-server']['IncomingMessagesRequest']; 107 const IncomingMessage = protobuf.roots['push-server']['IncomingMessage']; 108 const Receiver = protobuf.roots['push-server']['Receiver']; 109 110 const JSON_RPC_VERSION = "2.0" 111 const JSON_RPC_PING = "ping" 112 const JSON_RPC_PONG = "pong" 113 114 const PING_TIMEOUT = 10; 115 116 const RpcError = { 117 Parse: {code: -32700, message: "Parse error"}, 118 InvalidRequest: {code: -32600, message: "Invalid Request"}, 119 MethodNotFound: {code: -32601, message: "Method not found"}, 120 InvalidParams: {code: -32602, message: "Invalid params"}, 121 Internal: {code: -32603, message: "Internal error"}, 122 }; 123 124 const RpcMethod = { 125 Publish: "publish", 126 GetUsersLastSeen: "getUsersLastSeen", 127 Ping: "ping", 128 ListChannels: "listChannels", 129 SubscribeStatusChange: "subscribeStatusChange", 130 UnsubscribeStatusChange: "unsubscribeStatusChange", 131 } 132 133 class PullClient 134 { 135 constructor(params) 136 { 137 params = params || {}; 138 139 if (params.restApplication) 140 { 141 if (typeof params.configGetMethod === 'undefined') 142 { 143 params.configGetMethod = 'pull.application.config.get'; 144 } 145 if (typeof params.skipCheckRevision === 'undefined') 146 { 147 params.skipCheckRevision = true; 148 } 149 if (typeof params.restApplication === 'string') 150 { 151 params.siteId = params.restApplication; 152 } 153 154 params.serverEnabled = true; 155 } 156 157 this._status = PullStatus.Offline; 158 159 this.context = 'master'; 160 161 this.guestMode = params.guestMode ? params.guestMode : (typeof BX.message !== 'undefined' && BX.message.pull_guest_mode ? BX.message.pull_guest_mode === 'Y' : false); 162 this.guestUserId = params.guestUserId ? params.guestUserId : (typeof BX.message !== 'undefined' && BX.message.pull_guest_user_id ? parseInt(BX.message.pull_guest_user_id, 10) : 0); 163 if (this.guestMode && this.guestUserId) 164 { 165 this.userId = this.guestUserId; 166 } 167 else 168 {
169 this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0); 170 } 171 172 this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none'); 173 this.restClient = typeof params.restClient !== "undefined" ? params.restClient : new BX.RestClient(this.getRestClientOptions()); 174 175 this.enabled = typeof params.serverEnabled !== 'undefined' ? (params.serverEnabled === 'Y' || params.serverEnabled === true) : (typeof BX.message !== 'undefined' && BX.message.pull_server_enabled === 'Y'); 176 this.unloading = false; 177 this.starting = false; 178 this.debug = false; 179 this.connectionAttempt = 0; 180 this.connectionType = ConnectionType.WebSocket; 181 this.reconnectTimeout = null; 182 this.restartTimeout = null; 183 this.restoreWebSocketTimeout = null; 184 185 this.configGetMethod = typeof params.configGetMethod !== 'string' ? 'pull.config.get' : params.configGetMethod; 186 this.getPublicListMethod = typeof params.getPublicListMethod !== 'string' ? 'pull.channel.public.list' : params.getPublicListMethod; 187 188 this.skipStorageInit = params.skipStorageInit === true; 189 190 this.skipCheckRevision = params.skipCheckRevision === true; 191 192 this._subscribers = {}; 193 194 this.watchTagsQueue = {}; 195 this.watchUpdateInterval = 1740000; 196 this.watchForceUpdateInterval = 5000; 197 198 if (typeof params.configTimestamp !== 'undefined') 199 { 200 this.configTimestamp = params.configTimestamp; 201 } 202 else if (typeof BX.message !== 'undefined' && BX.message.pull_config_timestamp) 203 { 204 this.configTimestamp = BX.message.pull_config_timestamp; 205 } 206 else 207 { 208 this.configTimestamp = 0; 209 } 210 211 this.session = { 212 mid: null, 213 tag: null, 214 time: null, 215 history: {}, 216 lastMessageIds: [], 217 messageCount: 0 218 }; 219 220 this._connectors = { 221 webSocket: null, 222 longPolling: null 223 }; 224 225 this.isSecure = document.location.href.indexOf('https') === 0; 226 this.config = null; 227 228 this.storage = null; 229 230 if (this.userId && !this.skipStorageInit) 231 { 232 this.storage = new StorageManager({ 233 userId: this.userId, 234 siteId: this.siteId 235 }); 236 } 237 238 this.sharedConfig = new SharedConfig({ 239 onWebSocketBlockChanged: this.onWebSocketBlockChanged.bind(this), 240 storage: this.storage 241 }); 242 this.channelManager = new ChannelManager({ 243 restClient: this.restClient, 244 getPublicListMethod: this.getPublicListMethod 245 }); 246 247 this.notificationPopup = null; 248 249 // timers 250 this.checkInterval = null; 251 this.offlineTimeout = null; 252 253 this.pingWaitTimeout = null; 254 255 // manual stop workaround 256 this.isManualDisconnect = false; 257 258 this.loggingEnabled = this.sharedConfig.isLoggingEnabled(); 259 260 // bound event handlers 261 this.onPingTimeoutHandler = this.onPingTimeout.bind(this); 262 263 this.userStatusCallbacks = {}; // [userId] => array of callbacks 264 } 265 266 get connector() 267 { 268 return this._connectors[this.connectionType]; 269 } 270 271 get status() 272 { 273 return this._status; 274 } 275 276 set status(status) 277 { 278 if (this._status === status) 279 { 280 return; 281 } 282 283 this._status = status; 284 if (this.offlineTimeout) 285 { 286 clearTimeout(this.offlineTimeout) 287 this.offlineTimeout = null; 288 } 289 290 if (status === PullStatus.Offline) 291 { 292 this.sendPullStatusDelayed(status, OFFLINE_STATUS_DELAY); 293 } 294 else 295 { 296 this.sendPullStatus(status); 297 } 298 } 299 300 /** 301 * Creates a subscription to incoming messages. 302 * 303 * @param {Object} params 304 * @param {string} [params.type] Subscription type (for possible values see SubscriptionType). 305 * @param {string} [params.moduleId] Name of the module. 306 * @param {Function} params.callback Function, that will be called for incoming messages. 307 * @returns {Function} - Unsubscribe callback function 308 */ 309 subscribe(params) 310 { 311 /** 312 * After modify this method, copy to follow scripts: 313 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js 314 * mobile/install/js/mobile/pull/client/src/client.js 315 */ 316 317 if (!params) 318 { 319 console.error(Utils.getDateForLog() + ': Pull.subscribe: params for subscribe function is invali
319d. '); 320 return function () {} 321 } 322 323 if (!Utils.isPlainObject(params)) 324 { 325 return this.attachCommandHandler(params); 326 } 327 328 params = params || {}; 329 params.type = params.type || SubscriptionType.Server; 330 params.command = params.command || null; 331 332 if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client) 333 { 334 if (typeof (this._subscribers[params.type]) === 'undefined') 335 { 336 this._subscribers[params.type] = {}; 337 } 338 if (typeof (this._subscribers[params.type][params.moduleId]) === 'undefined') 339 { 340 this._subscribers[params.type][params.moduleId] = { 341 'callbacks': [], 342 'commands': {}, 343 }; 344 } 345 346 if (params.command) 347 { 348 if (typeof (this._subscribers[params.type][params.moduleId]['commands'][params.command]) === 'undefined') 349 { 350 this._subscribers[params.type][params.moduleId]['commands'][params.command] = []; 351 } 352 353 this._subscribers[params.type][params.moduleId]['commands'][params.command].push(params.callback); 354 355 return function () { 356 this._subscribers[params.type][params.moduleId]['commands'][params.command] = this._subscribers[params.type][params.moduleId]['commands'][params.command].filter((element) => { 357 return element !== params.callback; 358 }); 359 }.bind(this); 360 } 361 else 362 { 363 this._subscribers[params.type][params.moduleId]['callbacks'].push(params.callback); 364 365 return function () { 366 this._subscribers[params.type][params.moduleId]['callbacks'] = this._subscribers[params.type][params.moduleId]['callbacks'].filter((element) => { 367 return element !== params.callback; 368 }); 369 }.bind(this); 370 } 371 } 372 else 373 { 374 if (typeof (this._subscribers[params.type]) === 'undefined') 375 { 376 this._subscribers[params.type] = []; 377 } 378 379 this._subscribers[params.type].push(params.callback); 380 381 return function () { 382 this._subscribers[params.type] = this._subscribers[params.type].filter((element) => { 383 return element !== params.callback; 384 }); 385 }.bind(this); 386 } 387 } 388 389 attachCommandHandler(handler) 390 { 391 /** 392 * After modify this method, copy to follow scripts: 393 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js 394 */ 395 if (typeof handler.getModuleId !== 'function' || typeof handler.getModuleId() !== 'string') 396 { 397 console.error(Utils.getDateForLog() + ': Pull.attachCommandHandler: result of handler.getModuleId() is not a string.'); 398 return function () {} 399 } 400 401 let type = SubscriptionType.Server; 402 if (typeof handler.getSubscriptionType === 'function') 403 { 404 type = handler.getSubscriptionType(); 405 } 406 407 return this.subscribe({ 408 type: type, 409 moduleId: handler.getModuleId(), 410 callback: function (data) { 411 let method = null; 412 413 if (typeof handler.getMap === 'function') 414 { 415 const mapping = handler.getMap(); 416 if (mapping && typeof mapping === 'object') 417 { 418 if (typeof mapping[data.command] === 'function') 419 { 420 method = mapping[data.command].bind(handler) 421 } 422 else if (typeof mapping[data.command] === 'string' && typeof handler[mapping[data.command]] === 'function') 423 { 424 method = handler[mapping[data.command]].bind(handler); 425 } 426 } 427 } 428 429 if (!method) 430 { 431 const methodName = 'handle' + data.command.charAt(0).toUpperCase() + data.command.slice(1); 432 if (typeof handler[methodName] === 'function') 433 { 434 method = handler[methodName].bind(handler); 435 } 436 } 437 438 if (method) 439 { 440 if (this.debug && this.context !== 'master') 441 { 442 console.warn(Utils.getDateForLog() + ': Pull.attachCommandHandler: receive command', data); 443 } 444 method(data.params, data.extra, data.command); 445 } 446 }.bind(this) 447 }); 448 } 449 450 /** 451 * 452 * @param params {Object} 453 * @returns {boolean} 454 */ 455 emit(params) 456 { 457 /** 458 * After modify this method, copy to follow scripts: 459 * mobile/install/mobileapp/mobile/extensions/bitrix/pull/client/events/extension.js 460 * mobile/install/js/mobile/pull/client/src/client.js 461 */ 462 params = params || {}; 463 464 if (params.type == SubscriptionType.Server || params.type == SubscriptionType.Client) 465 { 466 if (typeof (this._subscribers[params.type]) === 'undefined') 467 { 468 this._subscribers[params.type] = {}; 469 } 470 if (typeof (this._subscribers[params.type][params.moduleId]) === 'undefined') 471 { 472 this._subscribers[params.type][params.moduleId] = { 473 'callbacks': [], 474 'commands': {}, 475 }; 476 } 477 478 if (this._subscribers[params.type][params.moduleId]['callbacks'].length > 0) 479 { 480 this._subscribers[params.type][params.moduleId]['callbacks'].forEach(function (callback) { 481 callback(params.data, {type: params.type, moduleId: params.moduleId}); 482 }); 483 } 484 485 if ( 486 this._subscribers[params.type][params.moduleId]['commands'][params.data.command] 487 && this._subscribers[params.type][params.moduleId]['commands'][params.data.command].length > 0) 488 { 489 this._subscribers[params.type][params.moduleId]['commands'][params.data.command].forEac
489h(function (callback) { 490 callback(params.data.params, params.data.extra, params.data.command, { 491 type: params.type, 492 moduleId: params.moduleId 493 }); 494 }); 495 } 496 497 return true; 498 } 499 else 500 { 501 if (typeof (this._subscribers[params.type]) === 'undefined') 502 { 503 this._subscribers[params.type] = []; 504 } 505 506 if (this._subscribers[params.type].length <= 0) 507 { 508 return true; 509 } 510 511 this._subscribers[params.type].forEach(function (callback) { 512 callback(params.data, {type: params.type}); 513 }); 514 515 return true; 516 } 517 } 518 519 init() 520 { 521 this._connectors.webSocket = new WebSocketConnector({ 522 parent: this, 523 onOpen: this.onWebSocketOpen.bind(this), 524 onMessage: this.onIncomingMessage.bind(this), 525 onDisconnect: this.onWebSocketDisconnect.bind(this), 526 onError: this.onWebSocketError.bind(this) 527 }); 528 529 this._connectors.longPolling = new LongPollingConnector({ 530 parent: this, 531 onOpen: this.onLongPollingOpen.bind(this), 532 onMessage: this.onIncomingMessage.bind(this), 533 onDisconnect: this.onLongPollingDisconnect.bind(this), 534 onError: this.onLongPollingError.bind(this) 535 }); 536 537 this.connectionType = this.isWebSocketAllowed() ? ConnectionType.WebSocket : ConnectionType.LongPolling; 538 539 window.addEventListener("beforeunload", this.onBeforeUnload.bind(this)); 540 window.addEventListener("offline", this.onOffline.bind(this)); 541 window.addEventListener("online", this.onOnline.bind(this)); 542 543 if (BX && BX.addCustomEvent) 544 { 545 BX.addCustomEvent("BXLinkOpened", this.connect.bind(this)); 546 } 547 548 if (BX && BX.desktop) 549 { 550 BX.addCustomEvent("onDesktopReload", () => { 551 this.session.mid = null; 552 this.session.tag = null; 553 this.session.time = null; 554 }); 555 556 BX.desktop.addCustomEvent("BXLoginSuccess", () => this.restart(1000, "desktop login")); 557 } 558 559 this.jsonRpcAdapter = new JsonRpc({ 560 connector: this._connectors.webSocket, 561 handlers: { 562 "incoming.message": this.handleRpcIncomingMessage.bind(this), 563 } 564 }); 565 } 566 567 start(config) 568 { 569 let allowConfigCaching = true; 570 571 if (this.isConnected()) 572 { 573 return Promise.resolve(true); 574 } 575 576 if (this.starting && this._startingPromise) 577 { 578 return this._startingPromise; 579 } 580 581 if (!this.userId && typeof (BX.message) !== 'undefined' && BX.message.USER_ID) 582 { 583 this.userId = BX.message.USER_ID; 584 if (!this.storage) 585 { 586 this.storage = new StorageManager({ 587 userId: this.userId, 588 siteId: this.siteId 589 }); 590 } 591 } 592 if (this.siteId === 'none' && typeof (BX.message) !== 'undefined' && BX.message.SITE_ID) 593 { 594 this.siteId = BX.message.SITE_ID; 595 } 596 597 let skipReconnectToLastSession = false; 598 if (Utils.isPlainObject(config)) 599 { 600 if (typeof config.skipReconnectToLastSession !== 'undefined') 601 { 602 skipReconnectToLastSession = !!config.skipReconnectToLastSession; 603 delete config.skipReconnectToLastSession; 604 } 605 this.config = config; 606 allowConfigCaching = false; 607 } 608 609 if (!this.enabled) 610 { 611 return Promise.reject({ 612 ex: {error: 'PULL_DISABLED', error_description: 'Push & Pull server is disabled'} 613 }); 614 } 615 616 const now = (new Date()).getTime(); 617 let oldSession; 618 if (!skipReconnectToLastSession && this.storage) 619 { 620 oldSession = this.storage.get(LS_SESSION); 621 } 622 if (Utils.isPlainObject(oldSession) && oldSession.hasOwnProperty('ttl') && oldSession.ttl >= now) 623 { 624 this.session.mid = oldSession.mid; 625 } 626 627 this.starting = true; 628 return new Promise((resolve, reject) => { 629 this._startingPromise = {resolve, reject}; 630 this.loadConfig("client_start").then( 631 (config) => { 632 this.setConfig(config, allowConfigCaching); 633 this.init(); 634 this.updateWatch(); 635 this.startCheckConfig(); 636 this.connect().then( 637 () => resolve(true), 638 error => reject(error) 639 ); 640 }, 641 (error) => { 642 this.starting = false; 643 this.status = PullStatus.Offline; 644 this.stopCheckConfig(); 645 console.error(Utils.getDateForLog() + ': Pull: could not read push-server config. ', error); 646 reject(error); 647 } 648 ); 649 }) 650 } 651 652 getRestClientOptions() 653 { 654 let result = {}; 655 656 if (this.guestMode && this.guestUserId !== 0) 657 { 658 result.queryParams = { 659 pull_guest_id: this.guestUserId 660 } 661 } 662 return result; 663 } 664 665 setLastMessageId(lastMessageId) 666 { 667 this.session.mid = lastMessageId; 668 } 669 670 /** 671 * 672 * @param {object[]} publicIds 673 * @param {integer} publicIds.user_id 674 * @param {string} publicIds.public_id 675 * @param {string} publicIds.signature 676 * @param {Date} publicIds.start 677 * @param {Date} publicIds.end 678 */ 679 setPublicIds(publicIds) 680 { 681 return this.channelManager.setPublicIds(publicIds); 682 } 683 684 /** 685 * Send single message to the specified users. 686 * 687 * @param {integer[]}
687 users User ids of the message receivers. 688 * @param {string} moduleId Name of the module to receive message, 689 * @param {string} command Command name. 690 * @param {object} params Command parameters. 691 * @param {integer} [expiry] Message expiry time in seconds. 692 * @return {Promise} 693 */ 694 sendMessage(users, moduleId, command, params, expiry) 695 { 696 const message = { 697 userList: users, 698 body: { 699 module_id: moduleId, 700 command: command, 701 params: params, 702 }, 703 expiry: expiry 704 }; 705 706 if (this.isJsonRpc()) 707 { 708 return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Publish, message) 709 } 710 else 711 { 712 return this.sendMessageBatch([message]); 713 } 714 } 715 716 /** 717 * Send single message to the specified public channels. 718 * 719 * @param {string[]} publicChannels Public ids of the channels to receive message. 720 * @param {string} moduleId Name of the module to receive message, 721 * @param {string} command Command name. 722 * @param {object} params Command parameters. 723 * @param {integer} [expiry] Message expiry time in seconds. 724 * @return {Promise} 725 */ 726 sendMessageToChannels(publicChannels, moduleId, command, params, expiry) 727 { 728 const message = { 729 channelList: publicChannels, 730 body: { 731 module_id: moduleId, 732 command: command, 733 params: params, 734 }, 735 expiry: expiry 736 }; 737 738 if (this.isJsonRpc()) 739 { 740 return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Publish, message) 741 } 742 else 743 { 744 return this.sendMessageBatch([message]); 745 } 746 } 747 748 /** 749 * Sends batch of messages to the multiple public channels. 750 * 751 * @param {object[]} messageBatch Array of messages to send. 752 * @param {int[]} messageBatch.userList User ids the message receivers. 753 * @param {string[]|object[]} messageBatch.channelList Public ids of the channels to send messages. 754 * @param {string} messageBatch.moduleId Name of the module to receive message, 755 * @param {string} messageBatch.command Command name. 756 * @param {object} messageBatch.params Command parameters. 757 * @param {integer} [messageBatch.expiry] Message expiry time in seconds. 758 * @return void 759 */ 760 sendMessageBatch(messageBatch) 761 { 762 if (!this.isPublishingEnabled()) 763 { 764 console.error('Client publishing is not supported or is disabled'); 765 return false; 766 } 767 768 if (this.isJsonRpc()) 769 { 770 let rpcRequest = this.jsonRpcAdapter.createPublishRequest(messageBatch); 771 return this.connector.send(JSON.stringify(rpcRequest)); 772 } 773 else 774 { 775 let userIds = {}; 776 for (let i = 0; i < messageBatch.length; i++) 777 { 778 if (messageBatch[i].userList) 779 { 780 for (let j = 0; j < messageBatch[i].userList.length; j++) 781 { 782 userIds[messageBatch[i].userList[j]] = true; 783 } 784 } 785 } 786 this.channelManager.getPublicIds(Object.keys(userIds)).then((publicIds) => { 787 return this.connector.send(this.encodeMessageBatch(messageBatch, publicIds)); 788 }) 789 } 790 } 791 792 encodeMessageBatch(messageBatch, publicIds) 793 { 794 let messages = []; 795 messageBatch.forEach(function (messageFields) { 796 const messageBody = messageFields.body; 797 798 let receivers; 799 if (messageFields.userList) 800 { 801 receivers = this.createMessageReceivers(messageFields.userList, publicIds); 802 } 803 else 804 { 805 receivers = []; 806 } 807 808 if (messageFields.channelList) 809 { 810 if (!Utils.isArray(messageFields.channelList)) 811 { 812 throw new Error('messageFields.publicChannels must be an array'); 813 } 814 messageFields.channelList.forEach(function (publicChannel) { 815 let publicId; 816 let signature; 817 if (typeof (publicChannel) === 'string' && publicChannel.includes('.')) 818 { 819 const fields = publicChannel.toString().split('.'); 820 publicId = fields[0]; 821 signature = fields[1]; 822 } 823 else if (typeof (publicChannel) === 'object' && ('publicId' in publicChannel) && ('signature' in publicChannel)) 824 { 825 publicId = publicChannel.publicId; 826 signature = publicChannel.signature; 827 } 828 else 829 { 830 throw new Error('Public channel MUST be either a string, formatted like "{publicId}.{signature}" or an object with fields \'publicId\' and \'signature\''); 831 } 832 833 receivers.push(Receiver.create({ 834 id: this.encodeId(publicId), 835 signature: this.encodeId(signature) 836 })) 837 }.bind(this)) 838 } 839 840 const message = IncomingMessage.create({ 841 receivers: receivers, 842 body: JSON.stringify(messageBody), 843 expiry: messageFields.expiry || 0 844 }); 845 messages.push(message); 846 }, this); 847 848 const requestBatch = RequestBatch.create({ 849 requests: [{ 850 incomingMessages: { 851 messages: messages 852 } 853 }] 854 }); 855 856 return RequestBatch.encode(requestBatch).finish(); 857 } 858 859 createMessageReceivers(users, publicIds) 860 { 861 let result = []; 862 for (let i = 0; i < users.length; i++) 863 { 864 let userId = users[i]; 865 if (!publicIds[userId] || !publicIds[userId].publicId) 866 { 867 throw new Error('Could not determine public id for user ' + userId); 868 } 869 870 result.push(Receiver.create({ 871 id: this.encodeId(publicIds[userId].publicId), 872 signature: this.encodeId(publicIds[userId].signature) 873 })); 874 } 875 return result; 876 } 877 878 /** 879 * @param userId {number} 880 * @param callback {UserStatusCallback} 881 * @returns {Promise} 882 */ 883 subscribeUserStatusChange(userId, callback) 884 { 885 if (typeof (userId) !== 'number') 886 { 887 throw new Error('userId must be a number'); 888 } 889 890 return new Promise((resolve, reject) => { 891 this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.SubscribeStatusChange, {userId}).then(() => { 892 if (!this.userStatusCallbacks[userId]) 893 { 894 this.userStatusCallbacks[userId] = []; 895 } 896 if (Utils.isFunction(callback)) 897 { 898 this.userStatusCallbacks[userId].push(callback); 899 } 900 901 return resolve() 902 }).catch(err => reject(err)) 903 }) 904 } 905 906 /** 907 * @param userId {number} 908 * @param callback {UserStatusCallback} 909 * @returns {Promise} 910 */ 911 unsubscribeUserStatusChange(userId, callback) 912 { 913 if (typeof (userId) !== 'number') 914 { 915 throw new Error('userId must be a number'); 916 } 917 if (this.userStatusCallbacks[userId]) 918 { 919 this.userStatusCallbacks[userId] = this.userStatusCallbacks[userId].filter(cb => cb !== callback) 920 if (this.userStatusCallbacks[userId].length === 0) 921 { 922 return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.UnsubscribeStatusChange, {userId}); 923 } 924 } 925 926 return Promise.resolve(); 927 } 928 929 emitUserStatusChange(userId, isOnline) 930 { 931 if (this.userStatusCallbacks[userId]) 932 {
933 this.userStatusCallbacks[userId].forEach(cb => cb({userId, isOnline})); 934 } 935 } 936 937 restoreUserStatusSubscription() 938 { 939 for (const userId in this.userStatusCallbacks) 940 { 941 if (this.userStatusCallbacks.hasOwnProperty(userId) && this.userStatusCallbacks[userId].length > 0) 942 { 943 this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.SubscribeStatusChange, {userId: Number(userId)}); 944 } 945 } 946 } 947 948 /** 949 * Returns "last seen" time in seconds for the users. Result format: Object{userId: int} 950 * If the user is currently connected - will return 0. 951 * If the user if offline - will return diff between current timestamp and last seen timestamp in seconds. 952 * If the user was never online - the record for user will be missing from the result object. 953 * 954 * @param {integer[]} userList List of user ids. 955 * @returns {Promise} 956 */ 957 getUsersLastSeen(userList) 958 { 959 if (!Utils.isArray(userList) || !userList.every(item => typeof (item) === 'number')) 960 { 961 throw new Error('userList must be an array of numbers'); 962 } 963 return new Promise((resolve, reject) => { 964 this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.GetUsersLastSeen, { 965 userList: userList 966 }).then(result => { 967 let unresolved = []; 968 for (let i = 0; i < userList.length; i++) 969 { 970 if (!result.hasOwnProperty(userList[i])) 971 { 972 unresolved.push(userList[i]); 973 } 974 } 975 if (unresolved.length === 0) 976 { 977 return resolve(result); 978 } 979 980 const params = { 981 userIds: unresolved, 982 sendToQueueSever: true 983 } 984 this.restClient.callMethod('pull.api.user.getLastSeen', params).then(response => { 985 let data = response.data(); 986 for (let userId in data) 987 { 988 result[userId] = data[userId]; 989 } 990 return resolve(result); 991 }).catch(error => { 992 console.error(error); 993 }) 994 }) 995 }) 996 } 997 998 /** 999 * Pings server. In case of success promise will be resolved, otherwise - rejected. 1000 * 1001 * @param {int} timeout Request timeout in seconds 1002 * @returns {Promise} 1003 */ 1004 ping(timeout) 1005 { 1006 return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.Ping, {}, timeout); 1007 } 1008 1009 /** 1010 * Returns list channels that the connection is subscribed to. 1011 * 1012 * @returns {Promise} 1013 */ 1014 listChannels() 1015 { 1016 return this.jsonRpcAdapter.executeOutgoingRpcCommand(RpcMethod.ListChannels, {}); 1017 } 1018 1019 scheduleRestart(disconnectCode, disconnectReason, restartDelay) 1020 { 1021 clearTimeout(this.restartTimeout); 1022 if (!restartDelay || restartDelay < 1) 1023 { 1024 restartDelay = Math.ceil(Math.random() * 30) + 5; 1025 } 1026 1027 this.restartTimeout = setTimeout( 1028 () => this.restart(disconnectCode, disconnectReason), 1029 restartDelay * 1000 1030 ); 1031 } 1032 1033 restart(disconnectCode, disconnectReason) 1034 { 1035 if (!disconnectCode) 1036 { 1037 disconnectCode = CloseReasons.NORMAL_CLOSURE; 1038 } 1039 clearTimeout(this.restartTimeout); 1040 console.warn(Utils.getDateForLog() + ': Pull: restarting with code ' + disconnectCode) 1041 this.disconnect(disconnectCode, disconnectReason); 1042 if (this.storage) 1043 { 1044 this.storage.remove('bx-pull-config'); 1045 } 1046 this.config = null; 1047 1048 const loadConfigReason = disconnectCode + '_' + disconnectReason.replaceAll(' ', '_'); 1049 this.loadConfig(loadConfigReason).then( 1050 (config) => { 1051 this.setConfig(config, true); 1052 this.updateWatch(); 1053 this.startCheckConfig(); 1054 this.connect().catch(error => console.error(error)); 1055 }, 1056 (error) => { 1057 console.error(Utils.getDateForLog() + ': Pull: could not read push-server config', error); 1058 this.status = PullStatus.Offline; 1059 1060 clearTimeout(this.reconnectTimeout); 1061 if (error.status == 401 || error.status == 403) 1062 { 1063 this.stopCheckConfig(); 1064 1065 if (BX && BX.onCustomEvent) 1066 { 1067 BX.onCustomEvent(window, 'onPullError', ['AUTHORIZE_ERROR']); 1068 } 1069 } 1070 } 1071 ); 1072 } 1073 1074 loadConfig(logTag) 1075 { 1076 if (!this.config) 1077 { 1078 this.config = Object.assign({}, EmptyConfig); 1079 1080 let config; 1081 if (this.storage) 1082 { 1083 config = this.storage.get('bx-pull-config'); 1084 } 1085 if (this.isConfigActual(config) && this.checkRevision(config.api.revision_web)) 1086 { 1087 return Promise.resolve(config); 1088 } 1089 else if (this.storage) 1090 { 1091 this.storage.remove('bx-pull-config') 1092 } 1093 } 1094 else if (this.isConfigActual(this.config) && this.checkRevision(this.config.api.revision_web)) 1095 { 1096 return Promise.resolve(this.config); 1097 } 1098 else 1099 {
1100 this.config = Object.assign({}, EmptyConfig); 1101 } 1102 1103 return new Promise((resolve, reject) => { 1104 this.restClient.callMethod(this.configGetMethod, {'CACHE': 'N'}, undefined, undefined, logTag).then((response) => { 1105 const data = response.data(); 1106 let timeShift; 1107 1108 timeShift = Math.floor((Utils.getTimestamp() - new Date(data.serverTime).getTime()) / 1000); 1109 delete data.serverTime; 1110 1111 let config = Object.assign({}, data); 1112 config.server.timeShift = timeShift; 1113 1114 resolve(config); 1115 }).catch((response) => { 1116 const error = response.error(); 1117 if (error.getError().error == "AUTHORIZE_ERROR" || error.getError().error == "WRONG_AUTH_TYPE") 1118 { 1119 error.status = 403; 1120 } 1121 reject(error); 1122 }); 1123 }) 1124 } 1125 1126 isConfigActual(config) 1127 { 1128 if (!Utils.isPlainObject(config)) 1129 { 1130 return false; 1131 } 1132 1133 if (config.server.config_timestamp < this.configTimestamp) 1134 { 1135 return false; 1136 } 1137 1138 const now = new Date(); 1139 1140 if (BX.type.isNumber(config.exp) && config.exp > 0 && config.exp < now.getTime() / 1000) 1141 { 1142 return false; 1143 } 1144 1145 const channelCount = Object.keys(config.channels).length; 1146 if (channelCount === 0) 1147 { 1148 return false; 1149 } 1150 1151 for (let channelType in config.channels) 1152 { 1153 if (!config.channels.hasOwnProperty(channelType)) 1154 { 1155 continue; 1156 } 1157 1158 const channel = config.channels[channelType]; 1159 const channelEnd = new Date(channel.end); 1160 1161 if (channelEnd < now) 1162 { 1163 return false; 1164 } 1165 } 1166 1167 return true; 1168 } 1169 1170 startCheckConfig() 1171 { 1172 if (this.checkInterval) 1173 { 1174 clearInterval(this.checkInterval); 1175 } 1176 1177 this.checkInterval = setInterval(this.checkConfig.bind(this), CONFIG_CHECK_INTERVAL) 1178 } 1179 1180 stopCheckConfig() 1181 { 1182 if (this.checkInterval) 1183 { 1184 clearInterval(this.checkInterval); 1185 } 1186 this.checkInterval = null; 1187 } 1188 1189 checkConfig() 1190 { 1191 if (this.isConfigActual(this.config)) 1192 { 1193 if (!this.checkRevision(this.config.api.revision_web)) 1194 { 1195 return false; 1196 } 1197 } 1198 else 1199 { 1200 this.logToConsole("Stale config detected. Restarting"); 1201 this.restart(CloseReasons.CONFIG_EXPIRED, "config expired"); 1202 } 1203 } 1204 1205 setConfig(config, allowCaching) 1206 { 1207 for (let key in config) 1208 { 1209 if (config.hasOwnProperty(key) && this.config.hasOwnProperty(key)) 1210 { 1211 this.config[key] = config[key]; 1212 } 1213 } 1214 1215 if (config.publicChannels) 1216 { 1217 this.setPublicIds(Utils.objectValues(config.publicChannels)); 1218 } 1219 1220 if (this.storage && allowCaching) 1221 { 1222 try 1223 { 1224 this.storage.set('bx-pull-config', config); 1225 } catch (e) 1226 { 1227 // try to delete the key "history" (landing site change history, see http://jabber.bx/view.php?id=136492) 1228 if (localStorage && localStorage.removeItem) 1229 { 1230 localStorage.removeItem('history'); 1231 } 1232 console.error(Utils.getDateForLog() + " Pull: Could not cache config in local storage. Error: ", e); 1233 } 1234 } 1235 } 1236 1237 isWebSocketSupported() 1238 { 1239 return typeof (window.WebSocket) !== "undefined"; 1240 } 1241 1242 isWebSocketAllowed() 1243 { 1244 if (this.sharedConfig.isWebSocketBlocked()) 1245 { 1246 return false; 1247 } 1248 1249 return this.isWebSocketEnabled(); 1250 } 1251 1252 isWebSocketEnabled() 1253 { 1254 if (!this.isWebSocketSupported()) 1255 { 1256 return false; 1257 } 1258 1259 return (this.config && this.config.server && this.config.server.websocket_enabled === true); 1260 } 1261 1262 isPublishingSupported() 1263 { 1264 return this.getServerVersion() > 3; 1265 } 1266 1267 isPublishingEnabled() 1268 { 1269 if (!this.isPublishingSupported()) 1270 { 1271 return false; 1272 } 1273 1274 return (this.config && this.config.server && this.config.server.publish_enabled === true); 1275 } 1276 1277 isProtobufSupported() 1278 { 1279 return (this.getServerVersion() == 4 && !Utils.browser.IsIe()); 1280 } 1281 1282 isJsonRpc() 1283 { 1284 return (this.getServerVersion() >= 5); 1285 } 1286 1287 isSharedMode() 1288 { 1289 return (this.getServerMode() == ServerMode.Shared) 1290 } 1291 1292 disconnect(disconnectCode, disconnectReason) 1293 { 1294 if (this.connector) 1295 { 1296 this.isManualDisconnect = true; 1297 this.connector.disconnect(disconnectCode, disconnectReason); 1298 } 1299 } 1300 1301 stop(disconnectCode, disconnectReason) 1302 { 1303 this.disconnect(disconnectCode, disconnectReason); 1304 this.stopCheckConfig(); 1305 } 1306 1307 reconnect(disconnectCode, disconnectReason, delay) 1308 { 1309 this.disconnect(disconnectCode, disconnectReason); 1310
1311 delay = delay || 1; 1312 this.scheduleReconnect(delay); 1313 } 1314 1315 restoreWebSocketConnection() 1316 { 1317 if (this.connectionType == ConnectionType.WebSocket) 1318 { 1319 return true; 1320 } 1321 1322 this._connectors.webSocket.connect(); 1323 } 1324 1325 scheduleReconnect(connectionDelay) 1326 { 1327 if (!this.enabled) 1328 { 1329 return false; 1330 } 1331 1332 if (!connectionDelay) 1333 { 1334 // never fallback to long polling 1335 // TODO remove long polling support later 1336 /*if (this.connectionAttempt > 3 && this.connectionType === ConnectionType.WebSocket && !this.sharedConfig.isLongPollingBlocked()) 1337 { 1338 // Websocket seems to be closed by network filter. Trying to fallback to long polling 1339 this.sharedConfig.setWebSocketBlocked(true); 1340 this.connectionType = ConnectionType.LongPolling; 1341 this.connectionAttempt = 1; 1342 connectionDelay = 1; 1343 } 1344 else*/ 1345 { 1346 connectionDelay = this.getConnectionAttemptDelay(this.connectionAttempt); 1347 } 1348 } 1349 if (this.reconnectTimeout) 1350 { 1351 clearTimeout(this.reconnectTimeout); 1352 } 1353 1354 this.logToConsole('Pull: scheduling reconnection in ' + connectionDelay + ' seconds; attempt # ' + this.connectionAttempt); 1355 1356 this.reconnectTimeout = setTimeout( 1357 () => { 1358 this.connect().catch(error => { 1359 console.error(error) 1360 }) 1361 }, 1362 connectionDelay * 1000); 1363 } 1364 1365 scheduleRestoreWebSocketConnection() 1366 { 1367 this.logToConsole('Pull: scheduling restoration of websocket connection in ' + RESTORE_WEBSOCKET_TIMEOUT + ' seconds'); 1368 1369 if (this.restoreWebSocketTimeout) 1370 { 1371 return; 1372 } 1373 1374 this.restoreWebSocketTimeout = setTimeout(() => { 1375 this.restoreWebSocketTimeout = 0; 1376 this.restoreWebSocketConnection(); 1377 }, RESTORE_WEBSOCKET_TIMEOUT * 1000); 1378 } 1379 1380 /** 1381 * @returns {Promise} 1382 */ 1383 connect() 1384 { 1385 if (!this.enabled) 1386 { 1387 return Promise.reject(); 1388 } 1389 if (this.connector.connected) 1390 { 1391 return Promise.resolve(); 1392 } 1393 1394 if (this.reconnectTimeout) 1395 { 1396 clearTimeout(this.reconnectTimeout); 1397 } 1398 1399 this.status = PullStatus.Connecting; 1400 this.connectionAttempt++; 1401 return new Promise((resolve, reject) => { 1402 this._connectPromise = {resolve, reject} 1403 this.connector.connect(); 1404 }) 1405 } 1406 1407 onIncomingMessage(message) 1408 { 1409 if (this.isJsonRpc()) 1410 { 1411 (message === JSON_RPC_PING) ? this.onJsonRpcPing() : this.jsonRpcAdapter.parseJsonRpcMessage(message); 1412 } 1413 else 1414 { 1415 const events = this.extractMessages(message); 1416 this.handleIncomingEvents(events); 1417 } 1418 } 1419 1420 handleRpcIncomingMessage(messageFields) 1421 { 1422 this.session.mid = messageFields.mid; 1423 let body = messageFields.body; 1424 1425 if (!messageFields.body.extra) 1426 { 1427 body.extra = {}; 1428 } 1429 body.extra.sender = messageFields.sender; 1430 1431 if ("user_params" in messageFields && Utils.isPlainObject(messageFields.user_params)) 1432 { 1433 Object.assign(body.params, messageFields.user_params) 1434 } 1435 1436 if ("dictionary" in messageFields && Utils.isPlainObject(messageFields.dictionary)) 1437 { 1438 Object.assign(body.params, messageFields.dictionary) 1439 } 1440 1441 if (this.checkDuplicate(messageFields.mid)) 1442 { 1443 this.addMessageToStat(body); 1444 this.trimDuplicates(); 1445 this.broadcastMessage(body) 1446 } 1447 1448 this.connector.send(`mack:${messageFields.mid}`) 1449 1450 return {}; 1451 } 1452 1453 onJsonRpcPing() 1454 { 1455 this.updatePingWaitTimeout(); 1456 this.connector.send(JSON_RPC_PONG) 1457 } 1458 1459 handleIncomingEvents(events) 1460 { 1461 let messages = []; 1462 if (events.length === 0) 1463 { 1464 this.session.mid = null; 1465 return; 1466 } 1467 1468 for (let i = 0; i < events.length; i++) 1469 { 1470 let event = events[i]; 1471 this.updateSessionFromEvent(event); 1472 if (event.mid && !this.checkDuplicate(event.mid)) 1473 { 1474 continue; 1475 } 1476 1477 this.addMessageToStat(event.text); 1478 messages.push(event.text); 1479 } 1480 this.trimDuplicates(); 1481 this.broadcastMessages(messages); 1482 } 1483 1484 updateSessionFromEvent(event) 1485 { 1486 this.session.mid = event.mid || null; 1487 this.session.tag = event.tag || null; 1488 this.session.time = event.time || null; 1489 } 1490 1491 checkDuplicate(mid) 1492 { 1493 if (this.session.lastMessageIds.includes(mid)) 1494 { 1495 console.warn("Duplicate message " + mid + " skipped"); 1496 return false;
1497 } 1498 else 1499 { 1500 this.session.lastMessageIds.push(mid); 1501 return true; 1502 } 1503 } 1504 1505 trimDuplicates() 1506 { 1507 if (this.session.lastMessageIds.length > MAX_IDS_TO_STORE) 1508 { 1509 this.session.lastMessageIds = this.session.lastMessageIds.slice(-MAX_IDS_TO_STORE); 1510 } 1511 } 1512 1513 addMessageToStat(message) 1514 { 1515 if (!this.session.history[message.module_id]) 1516 { 1517 this.session.history[message.module_id] = {}; 1518 } 1519 if (!this.session.history[message.module_id][message.command]) 1520 { 1521 this.session.history[message.module_id][message.command] = 0; 1522 } 1523 this.session.history[message.module_id][message.command]++; 1524 1525 this.session.messageCount++; 1526 } 1527 1528 extractMessages(pullEvent) 1529 { 1530 if (pullEvent instanceof ArrayBuffer) 1531 { 1532 return this.extractProtobufMessages(pullEvent); 1533 } 1534 else if (Utils.isNotEmptyString(pullEvent)) 1535 { 1536 return this.extractPlainTextMessages(pullEvent) 1537 } 1538 } 1539 1540 extractProtobufMessages(pullEvent) 1541 { 1542 let result = []; 1543 try 1544 { 1545 let responseBatch = ResponseBatch.decode(new Uint8Array(pullEvent)); 1546 for (let i = 0; i < responseBatch.responses.length; i++) 1547 { 1548 let response = responseBatch.responses[i]; 1549 if (response.command != "outgoingMessages") 1550 { 1551 continue; 1552 } 1553 1554 let messages = response.outgoingMessages.messages; 1555 for (let m = 0; m < messages.length; m++) 1556 { 1557 const message = messages[m]; 1558 let messageFields; 1559 try 1560 { 1561 messageFields = JSON.parse(message.body) 1562 } catch (e) 1563 { 1564 console.error(Utils.getDateForLog() + ": Pull: Could not parse message body", e); 1565 continue; 1566 } 1567 1568 if (!messageFields.extra) 1569 { 1570 messageFields.extra = {} 1571 } 1572 messageFields.extra.sender = { 1573 type: message.sender.type 1574 }; 1575 1576 if (message.sender.id instanceof Uint8Array) 1577 { 1578 messageFields.extra.sender.id = this.decodeId(message.sender.id) 1579 } 1580 1581 const compatibleMessage = { 1582 mid: this.decodeId(message.id), 1583 text: messageFields 1584 }; 1585 1586 result.push(compatibleMessage); 1587 } 1588 } 1589 } catch (e) 1590 { 1591 console.error(Utils.getDateForLog() + ": Pull: Could not parse message", e) 1592 } 1593 return result; 1594 } 1595 1596 extractPlainTextMessages(pullEvent) 1597 { 1598 let result = []; 1599 const dataArray = pullEvent.match(/#!NGINXNMS!#(.*?)#!NGINXNME!#/gm); 1600 if (dataArray === null) 1601 { 1602 const text = "\n========= PULL ERROR ===========\n" + 1603 "Error type: parseResponse error parsing message\n" + 1604 "\n" + 1605 "Data string: " + pullEvent + "\n" + 1606 "================================\n\n"; 1607 console.warn(text); 1608 return result; 1609 } 1610 for (let i = 0; i < dataArray.length; i++) 1611 { 1612 dataArray[i] = dataArray[i].substring(12, dataArray[i].length - 12); 1613 if (dataArray[i].length <= 0) 1614 { 1615 continue; 1616 } 1617 1618 let data 1619 try 1620 { 1621 data = JSON.parse(dataArray[i]) 1622 } catch (e) 1623 { 1624 continue; 1625 } 1626 1627 result.push(data); 1628 } 1629 return result; 1630 } 1631 1632 /** 1633 * Converts message id from byte[] to string 1634 * @param {Uint8Array} encodedId 1635 * @return {string} 1636 */ 1637 decodeId(encodedId) 1638 { 1639 if (!(encodedId instanceof Uint8Array)) 1640 { 1641 throw new Error("encodedId should be an instance of Uint8Array"); 1642 } 1643 1644 let result = ""; 1645 for (let i = 0; i < encodedId.length; i++) 1646 { 1647 const hexByte = encodedId[i].toString(16); 1648 if (hexByte.length === 1) 1649 { 1650 result += '0'; 1651 } 1652 result += hexByte; 1653 } 1654 return result; 1655 } 1656 1657 /** 1658 * Converts message id from hex-encoded string to byte[] 1659 * @param {string} id Hex-encoded string. 1660 * @return {Uint8Array} 1661 */ 1662 encodeId(id) 1663 { 1664 if (!id) 1665 { 1666 return new Uint8Array(); 1667 } 1668 1669 let result = []; 1670 for (let i = 0; i < id.length; i += 2) 1671 { 1672 result.push(parseInt(id.substr(i, 2), 16)); 1673 } 1674 1675 return new Uint8Array(result); 1676 } 1677 1678 broadcastMessages(messages) 1679 { 1680 messages.forEach(message => this.broadcastMessage(message)); 1681 } 1682 1683 broadcastMessage(message) 1684 { 1685 const moduleId = message.module_id = message.module_id.toLowerCase(); 1686 const command = message.command; 1687 1688 if (!message.extra) 1689 { 1690 message.extra = {}; 1691 } 1692 1693 if (message.extra.server_time_unix) 1694 { 1695 message.extra.server_time_ago = ((Utils.getTimestamp() - (message.extra.server_time_unix * 1000)) / 1000) - (this.config.server.timeShift ? this.config.server.timeShift : 0); 1696 message.extra.server_time_ago = message.extra.server_time_ago > 0 ? message.extra.server_time_ago : 0; 1697 } 1698 1699 this.logMessage(message); 1700 try 1701 { 1702 if (message.extra.sender && message.extra.sender.type === SenderType.Client) 1703 { 1704 if (typeof BX.onCustomEvent !== 'undefined') 1705 { 1706 BX.onCustomEvent(window, 'onPullClientEvent-' + moduleId, [command, message.params, message.extra], true); 1707 BX.onCustomEvent(window, 'onPullClientEvent', [moduleId, command, message.params, message.extra], true); 1708 } 1709
1710 this.emit({ 1711 type: SubscriptionType.Client, 1712 moduleId: moduleId, 1713 data: { 1714 command: command, 1715 params: Utils.clone(message.params), 1716 extra: Utils.clone(message.extra) 1717 } 1718 }); 1719 } 1720 else if (moduleId === 'pull') 1721 { 1722 this.handleInternalPullEvent(command, message); 1723 } 1724 else if (moduleId == 'online') 1725 { 1726 if (message.extra.server_time_ago < 240) 1727 { 1728 if (typeof BX.onCustomEvent !== 'undefined') 1729 { 1730 BX.onCustomEvent(window, 'onPullOnlineEvent', [command, message.params, message.extra], true); 1731 } 1732 1733 this.emit({ 1734 type: SubscriptionType.Online, 1735 data: { 1736 command: command, 1737 params: Utils.clone(message.params), 1738 extra: Utils.clone(message.extra) 1739 } 1740 }); 1741 } 1742 1743 if (command === 'userStatusChange') 1744 { 1745 this.emitUserStatusChange(message.params.user_id, message.params.online); 1746 } 1747 } 1748 else 1749 { 1750 if (typeof BX.onCustomEvent !== 'undefined') 1751 { 1752 BX.onCustomEvent(window, 'onPullEvent-' + moduleId, [command, message.params, message.extra], true); 1753 BX.onCustomEvent(window, 'onPullEvent', [moduleId, command, message.params, message.extra], true); 1754 } 1755 1756 this.emit({ 1757 type: SubscriptionType.Server, 1758 moduleId: moduleId, 1759 data: { 1760 command: command, 1761 params: Utils.clone(message.params), 1762 extra: Utils.clone(message.extra) 1763 } 1764 }); 1765 } 1766 } catch (e) 1767 { 1768 if (typeof (console) == 'object') 1769 { 1770 console.warn( 1771 "\n========= PULL ERROR ===========\n" + 1772 "Error type: broadcastMessages execute error\n" + 1773 "Error event: ", e, "\n" + 1774 "Message: ", message, "\n" + 1775 "================================\n" 1776 ); 1777 if (typeof BX.debug !== 'undefined') 1778 { 1779 BX.debug(e); 1780 } 1781 } 1782 } 1783 1784 if (message.extra && message.extra.revision_web) 1785 { 1786 this.checkRevision(message.extra.revision_web); 1787 } 1788 } 1789 1790 logToConsole(message, force) 1791 { 1792 if (this.loggingEnabled || force) 1793 { 1794 console.log(Utils.getDateForLog() + ': ' + message); 1795 } 1796 } 1797 1798 logMessage(message) 1799 { 1800 if (!this.debug) 1801 { 1802 return; 1803 } 1804 1805 if (message.extra.sender && message.extra.sender.type === SenderType.Client) 1806 { 1807 console.info('onPullClientEvent-' + message.module_id, message.command, message.params, message.extra); 1808 } 1809 else if (message.moduleId == 'online') 1810 { 1811 console.info('onPullOnlineEvent', message.command, message.params, message.extra); 1812 } 1813 else 1814 { 1815 console.info('onPullEvent', message.module_id, message.command, message.params, message.extra); 1816 } 1817 } 1818 1819 onLongPollingOpen() 1820 { 1821 this.unloading = false; 1822 this.starting = false; 1823 this.connectionAttempt = 0; 1824 this.isManualDisconnect = false; 1825 this.status = PullStatus.Online; 1826 1827 this.logToConsole('Pull: Long polling connection with push-server opened'); 1828 if (this.isWebSocketEnabled()) 1829 { 1830 this.scheduleRestoreWebSocketConnection(); 1831 } 1832 if (this._connectPromise) 1833 { 1834 this._connectPromise.resolve(); 1835 } 1836 } 1837 1838 onWebSocketBlockChanged(e) 1839 { 1840 const isWebSocketBlocked = e.isWebSocketBlocked; 1841 1842 if (isWebSocketBlocked && this.connectionType === ConnectionType.WebSocket && !this.isConnected()) 1843 { 1844 clearTimeout(this.reconnectTimeout); 1845 1846 this.connectionAttempt = 0; 1847 this.connectionType = ConnectionType.LongPolling; 1848 this.scheduleReconnect(1); 1849 } 1850 else if (!isWebSocketBlocked && this.connectionType === ConnectionType.LongPolling) 1851 { 1852 clearTimeout(this.reconnectTimeout); 1853 clearTimeout(this.restoreWebSocketTimeout); 1854 1855 this.connectionAttempt = 0; 1856 this.connectionType = ConnectionType.WebSocket; 1857 this.scheduleReconnect(1); 1858 } 1859 } 1860 1861 onWebSocketOpen() 1862 { 1863 this.unloading = false; 1864 this.starting = false; 1865 this.connectionAttempt = 0; 1866 this.isManualDisconnect = false; 1867 this.status = PullStatus.Online; 1868 this.sharedConfig.setWebSocketBlocked(false); 1869 1870 // to prevent fallback to long polling in case of networking problems 1871 this.sharedConfig.setLongPollingBlocked(true); 1872 1873 if (this.connectionType == ConnectionType.LongPolling) 1874 { 1875 this.connectionType = ConnectionType.WebSocket; 1876 this._connectors.longPolling.disconnect(); 1877 } 1878 1879 if (this.restoreWebSocketTimeout) 1880 { 1881 clearTimeout(this.restoreWebSocketTimeout); 1882 this.restoreWebSocketTimeout = null; 1883 } 1884 this.logToConsole('Pull: Websocket connection with push-server opened'); 1885 if (this._connectPromise) 1886 { 1887 this._connectPromise.resolve(); 1888 }
1889 this.restoreUserStatusSubscription(); 1890 } 1891 1892 onWebSocketDisconnect(e) 1893 { 1894 if (this.connectionType === ConnectionType.WebSocket) 1895 { 1896 this.status = PullStatus.Offline; 1897 } 1898 1899 if (!e) 1900 { 1901 e = {}; 1902 } 1903 1904 this.logToConsole('Pull: Websocket connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason, true); 1905 if (!this.isManualDisconnect) 1906 { 1907 if (e.code == CloseReasons.WRONG_CHANNEL_ID) 1908 { 1909 this.scheduleRestart(CloseReasons.WRONG_CHANNEL_ID, "wrong channel signature"); 1910 } 1911 else 1912 { 1913 this.scheduleReconnect(); 1914 } 1915 } 1916 1917 // to prevent fallback to long polling in case of networking problems 1918 this.sharedConfig.setLongPollingBlocked(true); 1919 this.isManualDisconnect = false; 1920 1921 this.clearPingWaitTimeout(); 1922 } 1923 1924 onWebSocketError(e) 1925 { 1926 this.starting = false; 1927 if (this.connectionType === ConnectionType.WebSocket) 1928 { 1929 this.status = PullStatus.Offline; 1930 } 1931 1932 console.error(Utils.getDateForLog() + ": Pull: WebSocket connection error", e); 1933 this.scheduleReconnect(); 1934 if (this._connectPromise) 1935 { 1936 this._connectPromise.reject(); 1937 } 1938 1939 this.clearPingWaitTimeout(); 1940 } 1941 1942 onLongPollingDisconnect(e) 1943 { 1944 if (this.connectionType === ConnectionType.LongPolling) 1945 { 1946 this.status = PullStatus.Offline; 1947 } 1948 1949 if (!e) 1950 { 1951 e = {}; 1952 } 1953 1954 this.logToConsole('Pull: Long polling connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason); 1955 if (!this.isManualDisconnect) 1956 { 1957 this.scheduleReconnect(); 1958 } 1959 this.isManualDisconnect = false; 1960 this.clearPingWaitTimeout(); 1961 } 1962 1963 onLongPollingError(e) 1964 { 1965 this.starting = false; 1966 if (this.connectionType === ConnectionType.LongPolling) 1967 { 1968 this.status = PullStatus.Offline; 1969 } 1970 console.error(Utils.getDateForLog() + ': Pull: Long polling connection error', e); 1971 this.scheduleReconnect(); 1972 if (this._connectPromise) 1973 { 1974 this._connectPromise.reject(); 1975 } 1976 this.clearPingWaitTimeout(); 1977 } 1978 1979 isConnected() 1980 { 1981 return this.connector ? this.connector.connected : false; 1982 } 1983 1984 onBeforeUnload() 1985 { 1986 this.unloading = true; 1987 1988 const session = Utils.clone(this.session); 1989 session.ttl = (new Date()).getTime() + LS_SESSION_CACHE_TIME * 1000; 1990 if (this.storage) 1991 { 1992 try 1993 { 1994 this.storage.set(LS_SESSION, JSON.stringify(session), LS_SESSION_CACHE_TIME); 1995 } catch (e) 1996 { 1997 console.error(Utils.getDateForLog() + " Pull: Could not save session info in local storage. Error: ", e); 1998 } 1999 } 2000 2001 this.scheduleReconnect(15); 2002 } 2003 2004 onOffline() 2005 { 2006 this.disconnect("1000", "offline"); 2007 } 2008 2009 onOnline() 2010 { 2011 this.connect(); 2012 } 2013 2014 handleInternalPullEvent(command, message) 2015 { 2016 switch (command.toUpperCase()) 2017 { 2018 case SystemCommands.CHANNEL_EXPIRE: 2019 { 2020 if (message.params.action == 'reconnect') 2021 { 2022 this.config.channels[message.params.channel.type] = message.params.new_channel; 2023 this.logToConsole("Pull: new config for " + message.params.channel.type + " channel set:\n", this.config.channels[message.params.channel.type]); 2024 2025 this.reconnect(CloseReasons.CONFIG_REPLACED, "config was replaced"); 2026 } 2027 else 2028 { 2029 this.restart(CloseReasons.CHANNEL_EXPIRED, "channel expired received"); 2030 } 2031 break; 2032 } 2033 case SystemCommands.CONFIG_EXPIRE: 2034 { 2035 this.restart(CloseReasons.CONFIG_EXPIRED, "config expired received"); 2036 break; 2037 } 2038 case SystemCommands.SERVER_RESTART: 2039 { 2040 this.reconnect(CloseReasons.SERVER_RESTARTED, "server was restarted", 15); 2041 break; 2042 } 2043 default:// 2044 } 2045 } 2046 2047 checkRevision(serverRevision) 2048 { 2049 if (this.skipCheckRevision) 2050 { 2051 return true; 2052 } 2053 2054 serverRevision = parseInt(serverRevision); 2055 if (serverRevision > 0 && serverRevision != REVISION) 2056 { 2057 this.enabled = false; 2058 if (typeof BX.message !== 'undefined') 2059 { 2060 this.showNotification(BX.message('PULL_OLD_REVISION')); 2061 } 2062 this.disconnect(CloseReasons.NORMAL_CLOSURE, 'check_revision'); 2063 2064 if (typeof BX.onCustomEvent !== 'undefined') 2065 { 2066 BX.onCustomEvent(window, 'onPullRevisionUp', [serverRevision, REVISION]); 2067 } 2068
2069 this.emit({ 2070 type: SubscriptionType.Revision, 2071 data: { 2072 server: serverRevision, 2073 client: REVISION 2074 } 2075 }); 2076 2077 this.logToConsole("Pull revision changed from " + REVISION + " to " + serverRevision + ". Reload required"); 2078 2079 return false; 2080 } 2081 return true; 2082 } 2083 2084 showNotification(text) 2085 { 2086 if (this.notificationPopup || typeof BX.PopupWindow === 'undefined') 2087 { 2088 return; 2089 } 2090 2091 this.notificationPopup = new BX.PopupWindow('bx-notifier-popup-confirm', null, { 2092 zIndex: 200, 2093 autoHide: false, 2094 closeByEsc: false, 2095 overlay: true, 2096 content: BX.create("div", { 2097 props: {className: "bx-messenger-confirm"}, 2098 html: text 2099 }), 2100 buttons: [ 2101 new BX.PopupWindowButton({ 2102 text: BX.message('JS_CORE_WINDOW_CLOSE'), 2103 className: "popup-window-button-decline", 2104 events: { 2105 click: () => this.notificationPopup.close(), 2106 } 2107 }) 2108 ], 2109 events: { 2110 onPopupClose: () => this.notificationPopup.destroy(), 2111 onPopupDestroy: () => this.notificationPopup = null, 2112 } 2113 }); 2114 this.notificationPopup.show(); 2115 } 2116 2117 getRevision() 2118 { 2119 return (this.config && this.config.api) ? this.config.api.revision_web : null; 2120 } 2121 2122 getServerVersion() 2123 { 2124 return (this.config && this.config.server) ? this.config.server.version : 0; 2125 } 2126 2127 getServerMode() 2128 { 2129 return (this.config && this.config.server) ? this.config.server.mode : null; 2130 } 2131 2132 getConfig() 2133 { 2134 return this.config; 2135 } 2136 2137 getDebugInfo() 2138 { 2139 if (!JSON || !JSON.stringify) 2140 { 2141 return false; 2142 } 2143 2144 let configDump; 2145 if (this.config && this.config.channels) 2146 { 2147 configDump = { 2148 "ChannelID": (this.config.channels.private ? this.config.channels.private.id : "n/a"), 2149 "ChannelDie": (this.config.channels.private ? this.config.channels.private.end : "n/a"), 2150 "ChannelDieShared": ("shared" in this.config.channels ? this.config.channels.shared.end : "n/a"), 2151 }; 2152 } 2153 else 2154 { 2155 configDump = {"Config error": "config is not loaded"} 2156 } 2157 2158 let websocketMode = "-"; 2159 if (this._connectors.webSocket && this._connectors.webSocket.socket) 2160 { 2161 if (this.isJsonRpc()) 2162 { 2163 websocketMode = "json-rpc" 2164 } 2165 else 2166 { 2167 websocketMode = (this._connectors.webSocket.socket.url.search("binaryMode=true") != -1 ? "protobuf" : "text") 2168 } 2169 } 2170 2171 return { 2172 "UserId": this.userId + (this.userId > 0 ? '' : '(guest)'), 2173 "Guest userId": (this.guestMode && this.guestUserId !== 0 ? this.guestUserId : "-"), 2174 "Browser online": (navigator.onLine ? 'Y' : 'N'), 2175 "Connect": (this.isConnected() ? 'Y' : 'N'), 2176 "Server type": (this.isSharedMode() ? 'cloud' : 'local'), 2177 "WebSocket supported": (this.isWebSocketSupported() ? 'Y' : 'N'), 2178 "WebSocket connected": (this._connectors.webSocket && this._connectors.webSocket.connected ? 'Y' : 'N'), 2179 "WebSocket mode": websocketMode, 2180 2181 "Try connect": (this.reconnectTimeout ? 'Y' : 'N'), 2182 "Try number": (this.connectionAttempt), 2183 2184 "Path": (this.connector ? this.connector.path : '-'), 2185 ...configDump, 2186 2187 "Last message": (this.session.mid > 0 ? this.session.mid : '-'), 2188 "Session history": this.session.history, 2189 "Watch tags": this.watchTagsQueue, 2190 } 2191 } 2192 2193 enableLogging(loggingFlag) 2194 { 2195 if (loggingFlag === undefined) 2196 { 2197 loggingFlag = true; 2198 } 2199 loggingFlag = loggingFlag === true; 2200 2201 this.sharedConfig.setLoggingEnabled(loggingFlag); 2202 this.loggingEnabled = loggingFlag; 2203 } 2204 2205 capturePullEvent(debugFlag) 2206 { 2207 if (debugFlag === undefined) 2208 { 2209 debugFlag = true; 2210 } 2211 2212 this.debug = debugFlag; 2213 } 2214 2215 getConnectionPath(connectionType) 2216 { 2217 let path; 2218 let params = {}; 2219 2220 switch (connectionType) 2221 { 2222 case ConnectionType.WebSocket: 2223 path = this.isSecure ? this.config.server.websocket_secure : this.config.server.websocket; 2224 break; 2225 case ConnectionType.LongPolling: 2226 path = this.isSecure ? this.config.server.long_pooling_secure : this.config.server.long_polling; 2227 break; 2228 default: 2229 throw new Error("Unknown connection type " + connectionType); 2230 } 2231 2232 if (!Utils.isNotEmptyString(path)) 2233 { 2234 return false;
2235 } 2236 2237 if (typeof (this.config.jwt) == 'string' && this.config.jwt !== '') 2238 { 2239 params['token'] = this.config.jwt; 2240 } 2241 else 2242 { 2243 let channels = []; 2244 ['private', 'shared'].forEach((type) => { 2245 if (typeof this.config.channels[type] !== 'undefined') 2246 { 2247 channels.push(this.config.channels[type].id); 2248 } 2249 }); 2250 if (channels.length === 0) 2251 { 2252 return false; 2253 } 2254 2255 params['CHANNEL_ID'] = channels.join('/'); 2256 } 2257 2258 if (this.isJsonRpc()) 2259 { 2260 params.jsonRpc = 'true'; 2261 } 2262 else if (this.isProtobufSupported()) 2263 { 2264 params.binaryMode = 'true'; 2265 } 2266 2267 if (this.isSharedMode()) 2268 { 2269 if (!this.config.clientId) 2270 { 2271 throw new Error("Push-server is in shared mode, but clientId is not set"); 2272 } 2273 params.clientId = this.config.clientId; 2274 } 2275 if (this.session.mid) 2276 { 2277 params.mid = this.session.mid; 2278 } 2279 if (this.session.tag) 2280 { 2281 params.tag = this.session.tag; 2282 } 2283 if (this.session.time) 2284 { 2285 params.time = this.session.time; 2286 } 2287 params.revision = REVISION; 2288 2289 return path + '?' + Utils.buildQueryString(params); 2290 } 2291 2292 getPublicationPath() 2293 { 2294 const path = this.isSecure ? this.config.server.publish_secure : this.config.server.publish; 2295 if (!path) 2296 { 2297 return ''; 2298 } 2299 2300 let channels = []; 2301 for (let type in this.config.channels) 2302 { 2303 if (!this.config.channels.hasOwnProperty(type)) 2304 { 2305 continue; 2306 } 2307 channels.push(this.config.channels[type].id); 2308 } 2309 2310 const params = { 2311 CHANNEL_ID: channels.join('/') 2312 }; 2313 2314 return path + '?' + Utils.buildQueryString(params); 2315 } 2316 2317 /** 2318 * Returns reconnect delay in seconds 2319 * @param attemptNumber 2320 * @return {number} 2321 */ 2322 getConnectionAttemptDelay(attemptNumber) 2323 { 2324 let result; 2325 if (attemptNumber < 1) 2326 { 2327 result = 0.5; 2328 } 2329 else if (attemptNumber < 3) 2330 { 2331 result = 15; 2332 } 2333 else if (attemptNumber < 5) 2334 { 2335 result = 45; 2336 } 2337 else if (attemptNumber < 10) 2338 { 2339 result = 600; 2340 } 2341 else 2342 { 2343 result = 3600; 2344 } 2345 2346 return result + (result * Math.random() * 0.2); 2347 } 2348 2349 sendPullStatusDelayed(status, delay) 2350 { 2351 if (this.offlineTimeout) 2352 { 2353 clearTimeout(this.offlineTimeout) 2354 } 2355 this.offlineTimeout = setTimeout( 2356 () => { 2357 this.offlineTimeout = null; 2358 this.sendPullStatus(status); 2359 }, 2360 delay 2361 ) 2362 } 2363 2364 sendPullStatus(status) 2365 { 2366 if (this.unloading) 2367 { 2368 return; 2369 } 2370 2371 if (typeof BX.onCustomEvent !== 'undefined') 2372 { 2373 BX.onCustomEvent(window, 'onPullStatus', [status]); 2374 } 2375 2376 this.emit({ 2377 type: SubscriptionType.Status, 2378 data: { 2379 status: status 2380 } 2381 }); 2382 } 2383 2384 extendWatch(tag, force) 2385 { 2386 if (!tag || this.watchTagsQueue[tag]) 2387 { 2388 return false; 2389 } 2390 2391 this.watchTagsQueue[tag] = true; 2392 if (force) 2393 { 2394 this.updateWatch(force); 2395 } 2396 } 2397 2398 updateWatch(force) 2399 { 2400 clearTimeout(this.watchUpdateTimeout); 2401 this.watchUpdateTimeout = setTimeout(() => { 2402 const watchTags = Object.keys(this.watchTagsQueue); 2403 if (watchTags.length > 0) 2404 { 2405 this.restClient.callMethod('pull.watch.extend', {tags: watchTags}, (result) => { 2406 if (result.error()) 2407 { 2408 this.updateWatch(); 2409 2410 return false; 2411 } 2412 2413 const updatedTags = result.data(); 2414 2415 for (let tagId in updatedTags) 2416 { 2417 if (updatedTags.hasOwnProperty(tagId) && !updatedTags[tagId]) 2418 { 2419 this.clearWatch(tagId); 2420 } 2421 } 2422 this.updateWatch(); 2423 }) 2424 } 2425 else 2426 { 2427 this.updateWatch(); 2428 } 2429 }, force ? this.watchForceUpdateInterval : this.watchUpdateInterval); 2430 } 2431 2432 clearWatch(tagId) 2433 { 2434 delete this.watchTagsQueue[tagId]; 2435 } 2436 2437 updatePingWaitTimeout() 2438 { 2439 clearTimeout(this.pingWaitTimeout); 2440 this.pingWaitTimeout = setTimeout(this.onPingTimeoutHandler, PING_TIMEOUT * 2 * 1000) 2441 } 2442 2443 clearPingWaitTimeout() 2444 { 2445 clearTimeout(this.pingWaitTimeout); 2446 this.pingWaitTimeout = null; 2447 } 2448 2449 onPingTimeout() 2450 { 2451 this.pingWaitTimeout = null; 2452 if (!this.enabled || !this.isConnected()) 2453 { 2454 return; 2455 } 2456 2457 console.warn("No pings are received in " + PING_TIMEOUT * 2 + " seconds. Reconnecting")
2458 this.disconnect(CloseReasons.STUCK, "connection stuck"); 2459 this.scheduleReconnect(); 2460 } 2461 2462 // old functions, not used anymore. 2463 setPrivateVar() {} 2464 2465 returnPrivateVar() {} 2466 2467 expireConfig() {} 2468 2469 updateChannelID() {} 2470 2471 tryConnect() {} 2472 2473 tryConnectDelay() {} 2474 2475 tryConnectSet() {} 2476 2477 updateState() {} 2478 2479 setUpdateStateStepCount() {} 2480 2481 supportWebSocket() 2482 { 2483 return this.isWebSocketSupported(); 2484 } 2485 2486 isWebSoketConnected() 2487 { 2488 return this.isConnected() && this.connectionType == ConnectionType.WebSocket; 2489 } 2490 2491 getPullServerStatus() {return this.isConnected()} 2492 2493 closeConfirm() 2494 { 2495 if (this.notificationPopup) 2496 { 2497 this.notificationPopup.destroy(); 2498 } 2499 } 2500 } 2501 2502 class SharedConfig 2503 { 2504 constructor(params) 2505 { 2506 params = params || {}; 2507 this.storage = params.storage || new StorageManager(); 2508 2509 this.ttl = 24 * 60 * 60; 2510 2511 this.lsKeys = { 2512 websocketBlocked: 'bx-pull-websocket-blocked', 2513 longPollingBlocked: 'bx-pull-longpolling-blocked', 2514 loggingEnabled: 'bx-pull-logging-enabled' 2515 }; 2516 2517 this.callbacks = { 2518 onWebSocketBlockChanged: (Utils.isFunction(params.onWebSocketBlockChanged) ? params.onWebSocketBlockChanged : function () {}) 2519 }; 2520 2521 if (this.storage) 2522 { 2523 window.addEventListener('storage', this.onLocalStorageSet.bind(this)); 2524 } 2525 } 2526 2527 onLocalStorageSet(params) 2528 { 2529 if ( 2530 this.storage.compareKey(params.key, this.lsKeys.websocketBlocked) 2531 && params.newValue != params.oldValue 2532 ) 2533 { 2534 this.callbacks.onWebSocketBlockChanged({ 2535 isWebSocketBlocked: this.isWebSocketBlocked() 2536 }) 2537 } 2538 } 2539 2540 isWebSocketBlocked() 2541 { 2542 if (!this.storage) 2543 { 2544 return false; 2545 } 2546 2547 return this.storage.get(this.lsKeys.websocketBlocked, 0) > Utils.getTimestamp(); 2548 } 2549 2550 setWebSocketBlocked(isWebSocketBlocked) 2551 { 2552 if (!this.storage) 2553 { 2554 return false; 2555 } 2556 2557 try 2558 { 2559 this.storage.set(this.lsKeys.websocketBlocked, (isWebSocketBlocked ? Utils.getTimestamp() + this.ttl : 0)); 2560 } catch (e) 2561 { 2562 console.error(Utils.getDateForLog() + " Pull: Could not save WS_blocked flag in local storage. Error: ", e); 2563 } 2564 } 2565 2566 isLongPollingBlocked() 2567 { 2568 if (!this.storage) 2569 { 2570 return false; 2571 } 2572 2573 return this.storage.get(this.lsKeys.longPollingBlocked, 0) > Utils.getTimestamp(); 2574 } 2575 2576 setLongPollingBlocked(isLongPollingBlocked) 2577 { 2578 if (!this.storage) 2579 { 2580 return false; 2581 } 2582 2583 try 2584 { 2585 this.storage.set(this.lsKeys.longPollingBlocked, (isLongPollingBlocked ? Utils.getTimestamp() + this.ttl : 0)); 2586 } catch (e) 2587 { 2588 console.error(Utils.getDateForLog() + " Pull: Could not save LP_blocked flag in local storage. Error: ", e); 2589 } 2590 } 2591 2592 isLoggingEnabled() 2593 { 2594 if (!this.storage) 2595 { 2596 return false; 2597 } 2598 2599 return this.storage.get(this.lsKeys.loggingEnabled, 0) > Utils.getTimestamp(); 2600 } 2601 2602 setLoggingEnabled(isLoggingEnabled) 2603 { 2604 if (!this.storage) 2605 { 2606 return false; 2607 } 2608 2609 try 2610 { 2611 this.storage.set(this.lsKeys.loggingEnabled, (isLoggingEnabled ? Utils.getTimestamp() + this.ttl : 0)); 2612 } catch (e) 2613 { 2614 console.error("LocalStorage error: ", e); 2615 return false; 2616 } 2617 } 2618 } 2619 2620 class AbstractConnector 2621 { 2622 _connected = false; 2623 connectionType = ""; 2624 2625 disconnectCode = ''; 2626 disconnectReason = ''; 2627 2628 constructor(config) 2629 { 2630 this.parent = config.parent; 2631 this.callbacks = { 2632 onOpen: Utils.isFunction(config.onOpen) ? config.onOpen : function () {}, 2633 onDisconnect: Utils.isFunction(config.onDisconnect) ? config.onDisconnect : function () {}, 2634 onError: Utils.isFunction(config.onError) ? config.onError : function () {}, 2635 onMessage: Utils.isFunction(config.onMessage) ? config.onMessage : function () {} 2636 }; 2637 } 2638 2639 get connected() 2640 { 2641 return this._connected 2642 } 2643 2644 set connected(value) 2645 { 2646 if (value == this._connected) 2647 { 2648 return; 2649 } 2650 2651 this._connected = value; 2652 2653 if (this._connected) 2654 { 2655 this.callbacks.onOpen(); 2656 } 2657 else 2658 { 2659 this.callbacks.onDisconnect({ 2660 code: this.disconnectCode, 2661 reason: this.disconnectReason 2662 }); 2663 } 2664 } 2665 2666 get path() 2667 { 2668 return this.parent.getConnectionPath(this.connectionType); 2669 } 2670 } 2671 2672 class WebSocketConnector extends AbstractConnector 2673 { 2674 constructor(config) 2675 { 2676 super(config) 2677 this.connectionType = ConnectionType.WebSocket; 2678 this.socket = null; 2679 2680 this.onSocketOpenHandler = this.onSocketOpen.bind(this); 2681 this.onSocketCloseHandler = this.onSocketClose.bind(this); 2682 this.onSocketErrorHandler = this.onSocketError.bind(this); 2683 this.onSocketMessageHandler = this.onSocketMessage.bind(this); 2684 } 2685 2686 connect() 2687 { 2688 if (this.socket) 2689 { 2690 if (this.socket.readyState === 1) 2691 { 2692 // already connected 2693 return true; 2694 } 2695 else 2696 { 2697 this.socket.removeEventListener('open', this.onSocketOpenHandler); 2698 this.socket.removeEventListener('close', this.onSocketCloseHandler); 2699 this.socket.removeEventListener('error', this.onSocketErrorHandler); 2700 this.socket.removeEventListener('message', this.onSocketMessageHandler); 2701 2702 this.socket.close(); 2703 this.socket = null; 2704 } 2705 } 2706 2707 this.createSocket(); 2708 } 2709 2710 disconnect(code, message) 2711 { 2712 if (this.socket !== null) 2713 { 2714 this.socket.removeEventListener('open', this.onSocketOpenHandler); 2715 this.socket.removeEventListener('close', this.onSocketCloseHandler); 2716 this.socket.removeEventListener('error', this.onSocketErrorHandler); 2717 this.socket.removeEventListener('message', this.onSocketMessageHandler); 2718 2719 this.socket.close(code, message); 2720 } 2721 this.socket = null; 2722 this.disconnectCode = code; 2723 this.disconnectReason = message; 2724 this.connected = false;
2725 } 2726 2727 createSocket() 2728 { 2729 if (this.socket) 2730 { 2731 throw new Error("Socket already exists"); 2732 } 2733 2734 if (!this.path) 2735 { 2736 throw new Error("Websocket connection path is not defined"); 2737 } 2738 2739 this.socket = new WebSocket(this.path); 2740 this.socket.binaryType = 'arraybuffer'; 2741 2742 this.socket.addEventListener('open', this.onSocketOpenHandler); 2743 this.socket.addEventListener('close', this.onSocketCloseHandler); 2744 this.socket.addEventListener('error', this.onSocketErrorHandler); 2745 this.socket.addEventListener('message', this.onSocketMessageHandler); 2746 } 2747 2748 /** 2749 * Sends some data to the server via websocket connection. 2750 * @param {ArrayBuffer} buffer Data to send. 2751 * @return {boolean} 2752 */ 2753 send(buffer) 2754 { 2755 if (!this.socket || this.socket.readyState !== 1) 2756 { 2757 console.error(Utils.getDateForLog() + ": Pull: WebSocket is not connected"); 2758 return false; 2759 } 2760 2761 this.socket.send(buffer); 2762 return true; 2763 } 2764 2765 onSocketOpen() 2766 { 2767 this.connected = true; 2768 } 2769 2770 onSocketClose(e) 2771 { 2772 this.socket = null; 2773 this.disconnectCode = e.code; 2774 this.disconnectReason = e.reason; 2775 this.connected = false; 2776 } 2777 2778 onSocketError(e) 2779 { 2780 this.callbacks.onError(e); 2781 } 2782 2783 onSocketMessage(e) 2784 { 2785 this.callbacks.onMessage(e.data); 2786 } 2787 2788 destroy() 2789 { 2790 if (this.socket) 2791 { 2792 this.socket.close(); 2793 this.socket = null; 2794 } 2795 } 2796 } 2797
2798 class LongPollingConnector extends AbstractConnector 2799 { 2800 constructor(config) 2801 { 2802 super(config); 2803 2804 this.active = false; 2805 this.connectionType = ConnectionType.LongPolling; 2806 this.requestTimeout = null; 2807 this.failureTimeout = null; 2808 this.xhr = this.createXhr(); 2809 this.requestAborted = false; 2810 } 2811 2812 createXhr() 2813 { 2814 const result = new XMLHttpRequest(); 2815 if (this.parent.isProtobufSupported() && !this.parent.isJsonRpc()) 2816 { 2817 result.responseType = "arraybuffer"; 2818 } 2819 result.addEventListener("readystatechange", this.onXhrReadyStateChange.bind(this)); 2820 return result; 2821 } 2822 2823 connect() 2824 { 2825 this.active = true; 2826 this.performRequest(); 2827 } 2828 2829 disconnect(code, reason) 2830 { 2831 this.active = false; 2832 2833 if (this.failureTimeout) 2834 { 2835 clearTimeout(this.failureTimeout); 2836 this.failureTimeout = null; 2837 } 2838 if (this.requestTimeout) 2839 { 2840 clearTimeout(this.requestTimeout); 2841 this.requestTimeout = null; 2842 } 2843 2844 if (this.xhr) 2845 { 2846 this.requestAborted = true; 2847 this.xhr.abort(); 2848 } 2849 2850 this.disconnectCode = code; 2851 this.disconnectReason = reason; 2852 this.connected = false; 2853 } 2854 2855 performRequest() 2856 { 2857 if (!this.active) 2858 { 2859 return; 2860 } 2861 2862 if (!this.path) 2863 { 2864 throw new Error("Long polling connection path is not defined"); 2865 } 2866 if (this.xhr.readyState !== 0 && this.xhr.readyState !== 4) 2867 { 2868 return; 2869 } 2870 2871 clearTimeout(this.failureTimeout); 2872 clearTimeout(this.requestTimeout); 2873 2874 this.failureTimeout = setTimeout(() => { this.connected = true }, 5000); 2875 this.requestTimeout = setTimeout(this.onRequestTimeout.bind(this), LONG_POLLING_TIMEOUT * 1000); 2876 2877 this.xhr.open("GET", this.path); 2878 this.xhr.send(); 2879 } 2880 2881 onRequestTimeout() 2882 { 2883 this.requestAborted = true; 2884 this.xhr.abort(); 2885 this.performRequest(); 2886 } 2887 2888 onXhrReadyStateChange() 2889 { 2890 if (this.xhr.readyState === 4) 2891 { 2892 if (!this.requestAborted || this.xhr.status == 200) 2893 { 2894 this.onResponse(this.xhr.response); 2895 } 2896 this.requestAborted = false; 2897 } 2898 } 2899 2900 /** 2901 * Sends some data to the server via http request. 2902 * @param {ArrayBuffer} buffer Data to send. 2903 * @return {bool} 2904 */ 2905 send(buffer) 2906 { 2907 const path = this.parent.getPublicationPath(); 2908 if (!path) 2909 { 2910 console.error(Utils.getDateForLog() + ": Pull: publication path is empty"); 2911 return false; 2912 } 2913 2914 let xhr = new XMLHttpRequest(); 2915 xhr.open("POST", path); 2916 xhr.send(buffer); 2917 } 2918 2919 onResponse(response) 2920 { 2921 if (this.failureTimeout) 2922 { 2923 clearTimeout(this.failureTimeout); 2924 this.failureTimeout = 0; 2925 } 2926 if (this.requestTimeout) 2927 { 2928 clearTimeout(this.requestTimeout); 2929 this.requestTimeout = 0; 2930 } 2931 2932 if (this.xhr.status == 200) 2933 { 2934 this.connected = true; 2935 if (Utils.isNotEmptyString(response) || (response instanceof ArrayBuffer)) 2936 { 2937 this.callbacks.onMessage(response); 2938 } 2939 else 2940 { 2941 this.parent.session.mid = null; 2942 } 2943 this.performRequest(); 2944 } 2945 else if (this.xhr.status == 304) 2946 { 2947 this.connected = true; 2948 if (this.xhr.getResponseHeader("Expires") === "Thu, 01 Jan 1973 11:11:01 GMT") 2949 { 2950 const lastMessageId = this.xhr.getResponseHeader("Last-Message-Id"); 2951 if (Utils.isNotEmptyString(lastMessageId)) 2952 { 2953 this.parent.setLastMessageId(lastMessageId); 2954 } 2955 } 2956 this.performRequest(); 2957 } 2958 else 2959 { 2960 this.callbacks.onError('Could not connect to the server'); 2961 this.connected = false; 2962 } 2963 } 2964 } 2965 2966 class ChannelManager 2967 { 2968 constructor(params) 2969 { 2970 this.publicIds = {}; 2971 2972 this.restClient = typeof params.restClient !== "undefined" ? params.restClient : BX.rest; 2973 2974 this.getPublicListMethod = params.getPublicListMethod; 2975 } 2976 2977 /** 2978 * 2979 * @param {Array} users Array of user ids. 2980 * @return {Promise} 2981 */ 2982 getPublicIds(users) 2983 { 2984 const now = new Date(); 2985 let result = {}; 2986 let unknownUsers = []; 2987 2988 for (let i = 0; i < users.length; i++) 2989 { 2990 const userId = users[i]; 2991 if (this.publicIds[userId] && this.publicIds[userId]['end'] > now) 2992 { 2993 result[userId] = this.publicIds[userId]; 2994 } 2995 else 2996 { 2997 unknownUsers.push(userId); 2998 } 2999 } 3000 3001 if (unknownUsers.length === 0) 3002 { 3003 return Promise.resolve(result); 3004 } 3005 3006 return new Promise((resolve) => { 3007 this.restClient.callMethod(this.getPublicListMethod, {users: unknownUsers}).then((response) => { 3008 if (response.error()) 3009 { 3010 return resolve({}); 3011 } 3012 3013 const data = response.data(); 3014 this.setPublicIds(Utils.objectValues(data));
3015 unknownUsers.forEach((userId) => { 3016 result[userId] = this.publicIds[userId]; 3017 }); 3018 3019 resolve(result); 3020 }); 3021 }) 3022 } 3023 3024 /** 3025 * 3026 * @param {object[]} publicIds 3027 * @param {integer} publicIds.user_id 3028 * @param {string} publicIds.public_id 3029 * @param {string} publicIds.signature 3030 * @param {Date} publicIds.start 3031 * @param {Date} publicIds.end 3032 */ 3033 setPublicIds(publicIds) 3034 { 3035 for (let i = 0; i < publicIds.length; i++) 3036 { 3037 const publicIdDescriptor = publicIds[i]; 3038 const userId = publicIdDescriptor.user_id; 3039 this.publicIds[userId] = { 3040 userId: userId, 3041 publicId: publicIdDescriptor.public_id, 3042 signature: publicIdDescriptor.signature, 3043 start: new Date(publicIdDescriptor.start), 3044 end: new Date(publicIdDescriptor.end) 3045 } 3046 } 3047 }; 3048 } 3049 3050 class StorageManager 3051 { 3052 constructor(params) 3053 { 3054 params = params || {}; 3055 3056 this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0); 3057 this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none'); 3058 } 3059 3060 set(name, value) 3061 { 3062 if (typeof window.localStorage === 'undefined') 3063 { 3064 return false; 3065 } 3066 if (typeof value != 'string') 3067 { 3068 if (value) 3069 { 3070 value = JSON.stringify(value); 3071 } 3072 } 3073 return window.localStorage.setItem(this.getKey(name), value) 3074 } 3075 3076 get(name, defaultValue) 3077 { 3078 if (typeof window.localStorage === 'undefined') 3079 { 3080 return defaultValue || null; 3081 } 3082 3083 const result = window.localStorage.getItem(this.getKey(name)); 3084 if (result === null) 3085 { 3086 return defaultValue || null; 3087 } 3088 3089 return JSON.parse(result); 3090 } 3091 3092 remove(name) 3093 { 3094 if (typeof window.localStorage === 'undefined') 3095 { 3096 return false; 3097 } 3098 return window.localStorage.removeItem(this.getKey(name)); 3099 } 3100 3101 getKey(name) 3102 { 3103 return 'bx-pull-' + this.userId + '-' + this.siteId + '-' + name; 3104 } 3105 3106 compareKey(eventKey, userKey) 3107 {
3108 return eventKey === this.getKey(userKey); 3109 } 3110 } 3111 3112 class JsonRpc 3113 { 3114 idCounter = 0; 3115 3116 handlers = {}; 3117 rpcResponseAwaiters = new Map(); 3118 3119 constructor(options) 3120 { 3121 this.connector = options.connector; 3122 if (Utils.isPlainObject(options.handlers)) 3123 { 3124 for (let method in options.handlers) 3125 { 3126 this.handle(method, options.handlers[method]); 3127 } 3128 } 3129 } 3130 3131 /** 3132 * @param {string} method 3133 * @param {function} handler 3134 */ 3135 handle(method, handler) 3136 { 3137 this.handlers[method] = handler; 3138 } 3139 3140 /** 3141 * Sends RPC command to the server. 3142 * 3143 * @param {string} method Method name 3144 * @param {object} params 3145 * @param {int} timeout 3146 * @returns {Promise} 3147 */ 3148 executeOutgoingRpcCommand(method, params, timeout) 3149 { 3150 if (!timeout) 3151 { 3152 timeout = 5; 3153 } 3154 return new Promise((resolve, reject) => { 3155 const request = this.createRequest(method, params); 3156 3157 if (!this.connector.send(JSON.stringify(request))) 3158 { 3159 reject(new ErrorNotConnected('websocket is not connected')); 3160 } 3161 3162 const t = setTimeout(() => { 3163 this.rpcResponseAwaiters.delete(request.id); 3164 reject(new ErrorTimeout('no response')); 3165 }, timeout * 1000); 3166 this.rpcResponseAwaiters.set(request.id, {resolve, reject, timeout: t}); 3167 }) 3168 } 3169 3170 /** 3171 * Executes array or rpc commands. Returns array of promises, each promise will be resolved individually. 3172 * 3173 * @param {JsonRpcRequest[]} batch 3174 * @returns {Promise[]} 3175 */ 3176 executeOutgoingRpcBatch(batch) 3177 { 3178 let requests = []; 3179 let promises = []; 3180 batch.forEach(({method, params, id}) => { 3181 const request = this.createRequest(method, params, id); 3182 requests.push(request); 3183 promises.push(new Promise((resolve, reject) => this.rpcResponseAwaiters.set(request.id, { 3184 resolve, 3185 reject 3186 }))); 3187 }); 3188 3189 this.connector.send(JSON.stringify(requests)); 3190 return promises; 3191 } 3192 3193 processRpcResponse(response) 3194 { 3195 if ("id" in response && this.rpcResponseAwaiters.has(response.id)) 3196 { 3197 const awaiter = this.rpcResponseAwaiters.get(response.id) 3198 if ("result" in response) 3199 { 3200 awaiter.resolve(response.result) 3201 } 3202 else if ("error" in response) 3203 { 3204 awaiter.reject(response.error) 3205 } 3206 else 3207 { 3208 awaiter.reject(new Error("wrong response structure")) 3209 } 3210 3211 clearTimeout(awaiter.timeout) 3212 this.rpcResponseAwaiters.delete(response.id) 3213 } 3214 else 3215 { 3216 console.error("Received rpc response with unknown id", response) 3217 } 3218 } 3219 3220 parseJsonRpcMessage(message) 3221 { 3222 let decoded 3223 try 3224 { 3225 decoded = JSON.parse(message); 3226 } catch (e) 3227 { 3228 console.error(Utils.getDateForLog() + ": Pull: Could not decode json rpc message", e); 3229 } 3230 3231 if (Utils.isArray(decoded)) 3232 { 3233 return this.executeIncomingRpcBatch(decoded); 3234 } 3235 else if (Utils.isJsonRpcRequest(decoded)) 3236 { 3237 return this.executeIncomingRpcCommand(decoded); 3238 } 3239 else if (Utils.isJsonRpcResponse(decoded)) 3240 { 3241 return this.processRpcResponse(decoded); 3242 } 3243 else 3244 { 3245 console.error(Utils.getDateForLog() + ": Pull: unknown rpc packet", decoded); 3246 } 3247 } 3248 3249 /** 3250 * Executes RPC command, received from the server 3251 * 3252 * @param {string} method 3253 * @param {object} params 3254 * @returns {object} 3255 */ 3256 executeIncomingRpcCommand({method, params}) 3257 { 3258 if (method in this.handlers) 3259 { 3260 return this.handlers[method].call(this, params) 3261 } 3262 3263 return { 3264 "error": RpcError.MethodNotFound 3265 } 3266 } 3267 3268 executeIncomingRpcBatch(batch) 3269 { 3270 let result = []; 3271 for (let command of batch) 3272 { 3273 if ("jsonrpc" in command) 3274 { 3275 if ("method" in command) 3276 { 3277 let commandResult = this.executeIncomingRpcCommand(command) 3278 if (commandResult) 3279 { 3280 commandResult["jsonrpc"] = JSON_RPC_VERSION; 3281 commandResult["id"] = command["id"]; 3282 3283 result.push(commandResult) 3284 } 3285 } 3286 else 3287 { 3288 this.processRpcResponse(command) 3289 } 3290 } 3291 else 3292 { 3293 console.error(Utils.getDateForLog() + ": Pull: unknown rpc command in batch", command); 3294 result.push({ 3295 "jsonrpc": "2.0", 3296 "error": RpcError.InvalidRequest, 3297 }) 3298 } 3299 } 3300 3301 return result; 3302 } 3303 3304 nextId() 3305 { 3306 return ++this.idCounter; 3307 } 3308 3309 createPublishRequest(messageBatch) 3310 { 3311 let result = messageBatch.map(message => this.createRequest('publish', message)); 3312 3313 if (result.length === 0) 3314 { 3315 return result[0] 3316 } 3317 3318 return result; 3319 } 3320 3321 createRequest(method, params, id) 3322 { 3323 if (!id) 3324 { 3325 id = this.nextId() 3326 } 3327 3328 return { 3329 jsonrpc: JSON_RPC_VERSION, 3330 method: method, 3331 params: params, 3332 id: id 3333 } 3334 } 3335 } 3336
3337 class ErrorNotConnected extends Error 3338 { 3339 constructor(message) 3340 { 3341 super(message); 3342 this.name = 'ErrorNotConnected'; 3343 } 3344 } 3345 3346 class ErrorTimeout extends Error 3347 { 3348 constructor(message) 3349 { 3350 super(message); 3351 this.name = 'ErrorTimeout'; 3352 } 3353 } 3354 3355 const Utils = { 3356 browser: { 3357 IsChrome: function () { 3358 return navigator.userAgent.toLowerCase().indexOf('chrome') != -1; 3359 }, 3360 IsFirefox: function () { 3361 return navigator.userAgent.toLowerCase().indexOf('firefox') != -1; 3362 }, 3363 IsIe: function () { 3364 return navigator.userAgent.match(/(Trident\/|MSIE\/)/) !== null; 3365 } 3366 }, 3367 getTimestamp: function () { 3368 return (new Date()).getTime(); 3369 }, 3370 /** 3371 * Reduces errors array to single string. 3372 * @param {array} errors 3373 * @return {string} 3374 */ 3375 errorsToString: function (errors) { 3376 if (!this.isArray(errors)) 3377 { 3378 return ""; 3379 } 3380 else 3381 { 3382 return errors.reduce(function (result, currentValue) { 3383 if (result != "") 3384 { 3385 result += "; "; 3386 } 3387 return result + currentValue.code + ": " + currentValue.message; 3388 }, ""); 3389 } 3390 }, 3391 isString: function (item) { 3392 return item === '' ? true : (item ? (typeof (item) == "string" || item instanceof String) : false); 3393 }, 3394 isArray: function (item) { 3395 return item && Object.prototype.toString.call(item) == "[object Array]"; 3396 }, 3397 isFunction: function (item) { 3398 return item === null ? false : (typeof (item) == "function" || item instanceof Function); 3399 }, 3400 isDomNode: function (item) { 3401 return item && typeof (item) == "object" && "nodeType" in item; 3402 }, 3403 isDate: function (item) { 3404 return item && Object.prototype.toString.call(item) == "[object Date]"; 3405 }, 3406 isPlainObject: function (item) { 3407 if (!item || typeof (item) !== "object" || item.nodeType) 3408 { 3409 return false; 3410 } 3411 3412 const hasProp = Object.prototype.hasOwnProperty; 3413 try 3414 { 3415 if (item.constructor && !hasProp.call(item, "constructor") && !hasProp.call(item.constructor.prototype, "isPrototypeOf")) 3416 { 3417 return false; 3418 } 3419 } catch (e) 3420 { 3421 return false; 3422 } 3423 3424 let key; 3425 for (key in item) 3426 { 3427 } 3428 return typeof (key) === "undefined" || hasProp.call(item, key); 3429 }, 3430 isNotEmptyString: function (item) { 3431 return this.isString(item) ? item.length > 0 : false; 3432 }, 3433 isJsonRpcRequest: function (item) { 3434 return ( 3435 typeof (item) === "object" 3436 && item 3437 && "jsonrpc" in item 3438 && Utils.isNotEmptyString(item.jsonrpc) 3439 && "method" in item 3440 && Utils.isNotEmptyString(item.method) 3441 ); 3442 }, 3443 isJsonRpcResponse: function (item) { 3444 return ( 3445 typeof (item) === "object" 3446 && item 3447 && "jsonrpc" in item 3448 && Utils.isNotEmptyString(item.jsonrpc) 3449 && "id" in item 3450 && ( 3451 "result" in item 3452 || "error" in item 3453 ) 3454 ); 3455 3456 }, 3457 buildQueryString: function (params) { 3458 let result = ''; 3459 for (let key in params) 3460 { 3461 if (!params.hasOwnProperty(key)) 3462 { 3463 continue; 3464 } 3465 const value = params[key]; 3466 if (Utils.isArray(value)) 3467 { 3468 value.forEach((valueElement, index) => { 3469 result += encodeURIComponent(key + "[" + index + "]") + "=" + encodeURIComponent(valueElement) + "&"; 3470 }); 3471 } 3472 else 3473 { 3474 result += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&"; 3475 } 3476 } 3477 3478 if (result.length > 0) 3479 { 3480 result = result.substr(0, result.length - 1); 3481 } 3482 3483 return result; 3484 }, 3485 objectValues: function values(obj) { 3486 let result = []; 3487 for (let key in obj) 3488 { 3489 if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key)) 3490 { 3491 result.push(obj[key]); 3492 } 3493 } 3494 return result; 3495 }, 3496 clone: function (obj, bCopyObj) { 3497 let _obj, i, l; 3498 if (bCopyObj !== false) 3499 { 3500 bCopyObj = true; 3501 } 3502 3503 if (obj === null) 3504 { 3505 return null; 3506 } 3507 3508 if (this.isDomNode(obj)) 3509 { 3510 _obj = obj.cloneNode(bCopyObj); 3511 } 3512 else if (typeof obj == 'object') 3513 { 3514 if (this.isArray(obj)) 3515 { 3516 _obj = []; 3517 for (i = 0, l = obj.length; i < l; i++) 3518 { 3519 if (typeof obj[i] == "object" && bCopyObj) 3520 { 3521 _obj[i] = this.clone(obj[i], bCopyObj); 3522 } 3523 else 3524 { 3525 _obj[i] = obj[i]; 3526 } 3527 } 3528 } 3529 else 3530 { 3531 _obj = {}; 3532 if (obj.constructor) 3533 { 3534 if (this.isDate(obj)) 3535 { 3536 _obj = new Date(obj); 3537 } 3538 else 3539 { 3540 _obj = new obj.constructor(); 3541 } 3542 } 3543 3544 for (i in obj) 3545 { 3546 if (!obj.hasOwnProperty(i)) 3547 { 3548 continue; 3549 } 3550 if (typeof obj[i] == "object" && bCopyObj) 3551 { 3552 _obj[i] = this.clone(obj[i], bCopyObj); 3553 } 3554 else 3555 { 3556 _obj[i] = obj[i]; 3557 } 3558 } 3559 } 3560 3561 } 3562 else 3563 {
3564 _obj = obj; 3565 } 3566 3567 return _obj; 3568 }, 3569 3570 getDateForLog: function () { 3571 const d = new Date(); 3572 3573 return d.getFullYear() + "-" + Utils.lpad(d.getMonth(), 2, '0') + "-" + Utils.lpad(d.getDate(), 2, '0') + " " + Utils.lpad(d.getHours(), 2, '0') + ":" + Utils.lpad(d.getMinutes(), 2, '0'); 3574 }, 3575 3576 lpad: function (str, length, chr) { 3577 str = str.toString(); 3578 chr = chr || ' '; 3579 3580 if (str.length > length) 3581 { 3582 return str; 3583 } 3584 3585 let result = ''; 3586 for (let i = 0; i < length - str.length; i++) 3587 { 3588 result += chr; 3589 } 3590 3591 return result + str; 3592 } 3593 } 3594 3595 if ( 3596 typeof BX.namespace !== 'undefined' 3597 && typeof BX.PULL === 'undefined' 3598 ) 3599 { 3600 BX.PULL = new PullClient(); 3601 } 3602 3603 BX.PullClient = PullClient; 3604 BX.PullClient.PullStatus = PullStatus; 3605 BX.PullClient.SubscriptionType = SubscriptionType; 3606 BX.PullClient.CloseReasons = CloseReasons; 3607 BX.PullClient.StorageManager = StorageManager; 3608})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.