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 if (!disconnectReason) 1040 { 1041 disconnectReason = 'manual restart' 1042 } 1043 clearTimeout(this.restartTimeout); 1044 console.warn(Utils.getDateForLog() + ': Pull: restarting with code ' + disconnectCode) 1045 this.disconnect(disconnectCode, disconnectReason); 1046 if (this.storage) 1047 { 1048 this.storage.remove('bx-pull-config'); 1049 } 1050 this.config = null; 1051 1052 const loadConfigReason = disconnectCode + '_' + disconnectReason.replaceAll(' ', '_'); 1053 this.loadConfig(loadConfigReason).then( 1054 (config) => { 1055 this.setConfig(config, true); 1056 this.updateWatch(); 1057 this.startCheckConfig(); 1058 this.connect().catch(error => console.error(error)); 1059 }, 1060 (error) => { 1061 console.error(Utils.getDateForLog() + ': Pull: could not read push-server config', error); 1062 this.status = PullStatus.Offline; 1063 1064 clearTimeout(this.reconnectTimeout); 1065 if (error.status == 401 || error.status == 403) 1066 { 1067 this.stopCheckConfig(); 1068 1069 if (BX && BX.onCustomEvent) 1070 { 1071 BX.onCustomEvent(window, 'onPullError', ['AUTHORIZE_ERROR']); 1072 } 1073 } 1074 } 1075 ); 1076 } 1077 1078 loadConfig(logTag) 1079 { 1080 if (!this.config) 1081 { 1082 this.config = Object.assign({}, EmptyConfig); 1083 1084 let config; 1085 if (this.storage) 1086 { 1087 config = this.storage.get('bx-pull-config'); 1088 } 1089 if (this.isConfigActual(config) && this.checkRevision(config.api.revision_web)) 1090 { 1091 return Promise.resolve(config); 1092 } 1093 else if (this.storage) 1094 { 1095 this.storage.remove('bx-pull-config') 1096 } 1097 } 1098 else if (this.isConfigActual(this.config) && this.checkRevision(this.config.api.revision_web)) 1099 { 1100 return Promise.resolve(this.config); 1101 } 1102 else 1103 {
1104 this.config = Object.assign({}, EmptyConfig); 1105 } 1106 1107 return new Promise((resolve, reject) => { 1108 this.restClient.callMethod(this.configGetMethod, {'CACHE': 'N'}, undefined, undefined, logTag).then((response) => { 1109 const data = response.data(); 1110 let timeShift; 1111 1112 timeShift = Math.floor((Utils.getTimestamp() - new Date(data.serverTime).getTime()) / 1000); 1113 delete data.serverTime; 1114 1115 let config = Object.assign({}, data); 1116 config.server.timeShift = timeShift; 1117 1118 resolve(config); 1119 }).catch((response) => { 1120 const error = response.error(); 1121 if (error.getError().error == "AUTHORIZE_ERROR" || error.getError().error == "WRONG_AUTH_TYPE") 1122 { 1123 error.status = 403; 1124 } 1125 reject(error); 1126 }); 1127 }) 1128 } 1129 1130 isConfigActual(config) 1131 { 1132 if (!Utils.isPlainObject(config)) 1133 { 1134 return false; 1135 } 1136 1137 if (config.server.config_timestamp < this.configTimestamp) 1138 { 1139 return false; 1140 } 1141 1142 const now = new Date(); 1143 1144 if (BX.type.isNumber(config.exp) && config.exp > 0 && config.exp < now.getTime() / 1000) 1145 { 1146 return false; 1147 } 1148 1149 const channelCount = Object.keys(config.channels).length; 1150 if (channelCount === 0) 1151 { 1152 return false; 1153 } 1154 1155 for (let channelType in config.channels) 1156 { 1157 if (!config.channels.hasOwnProperty(channelType)) 1158 { 1159 continue; 1160 } 1161 1162 const channel = config.channels[channelType]; 1163 const channelEnd = new Date(channel.end); 1164 1165 if (channelEnd < now) 1166 { 1167 return false; 1168 } 1169 } 1170 1171 return true; 1172 } 1173 1174 startCheckConfig() 1175 { 1176 if (this.checkInterval) 1177 { 1178 clearInterval(this.checkInterval); 1179 } 1180 1181 this.checkInterval = setInterval(this.checkConfig.bind(this), CONFIG_CHECK_INTERVAL) 1182 } 1183 1184 stopCheckConfig() 1185 { 1186 if (this.checkInterval) 1187 { 1188 clearInterval(this.checkInterval); 1189 } 1190 this.checkInterval = null; 1191 } 1192 1193 checkConfig() 1194 { 1195 if (this.isConfigActual(this.config)) 1196 { 1197 if (!this.checkRevision(this.config.api.revision_web)) 1198 { 1199 return false; 1200 } 1201 } 1202 else 1203 { 1204 this.logToConsole("Stale config detected. Restarting"); 1205 this.restart(CloseReasons.CONFIG_EXPIRED, "config expired"); 1206 } 1207 } 1208 1209 setConfig(config, allowCaching) 1210 { 1211 for (let key in config) 1212 { 1213 if (config.hasOwnProperty(key) && this.config.hasOwnProperty(key)) 1214 { 1215 this.config[key] = config[key]; 1216 } 1217 } 1218 1219 if (config.publicChannels) 1220 { 1221 this.setPublicIds(Utils.objectValues(config.publicChannels)); 1222 } 1223 1224 if (this.storage && allowCaching) 1225 { 1226 try 1227 { 1228 this.storage.set('bx-pull-config', config); 1229 } catch (e) 1230 { 1231 // try to delete the key "history" (landing site change history, see http://jabber.bx/view.php?id=136492) 1232 if (localStorage && localStorage.removeItem) 1233 { 1234 localStorage.removeItem('history'); 1235 } 1236 console.error(Utils.getDateForLog() + " Pull: Could not cache config in local storage. Error: ", e); 1237 } 1238 } 1239 } 1240 1241 isWebSocketSupported() 1242 { 1243 return typeof (window.WebSocket) !== "undefined"; 1244 } 1245 1246 isWebSocketAllowed() 1247 { 1248 if (this.sharedConfig.isWebSocketBlocked()) 1249 { 1250 return false; 1251 } 1252 1253 return this.isWebSocketEnabled(); 1254 } 1255 1256 isWebSocketEnabled() 1257 { 1258 if (!this.isWebSocketSupported()) 1259 { 1260 return false; 1261 } 1262 1263 return (this.config && this.config.server && this.config.server.websocket_enabled === true); 1264 } 1265 1266 isPublishingSupported() 1267 { 1268 return this.getServerVersion() > 3; 1269 } 1270 1271 isPublishingEnabled() 1272 { 1273 if (!this.isPublishingSupported()) 1274 { 1275 return false; 1276 } 1277 1278 return (this.config && this.config.server && this.config.server.publish_enabled === true); 1279 } 1280 1281 isProtobufSupported() 1282 { 1283 return (this.getServerVersion() == 4 && !Utils.browser.IsIe()); 1284 } 1285 1286 isJsonRpc() 1287 { 1288 return (this.getServerVersion() >= 5); 1289 } 1290 1291 isSharedMode() 1292 { 1293 return (this.getServerMode() == ServerMode.Shared) 1294 } 1295 1296 disconnect(disconnectCode, disconnectReason) 1297 { 1298 if (this.connector) 1299 { 1300 this.isManualDisconnect = true; 1301 this.connector.disconnect(disconnectCode, disconnectReason); 1302 } 1303 } 1304 1305 stop(disconnectCode, disconnectReason) 1306 { 1307 this.disconnect(disconnectCode, disconnectReason); 1308 this.stopCheckConfig(); 1309 } 1310 1311 reconnect(disconnectCode, disconnectReason, delay) 1312 { 1313 this.disconnect(disconnectCode, disconnectReason); 1314
1315 delay = delay || 1; 1316 this.scheduleReconnect(delay); 1317 } 1318 1319 restoreWebSocketConnection() 1320 { 1321 if (this.connectionType == ConnectionType.WebSocket) 1322 { 1323 return true; 1324 } 1325 1326 this._connectors.webSocket.connect(); 1327 } 1328 1329 scheduleReconnect(connectionDelay) 1330 { 1331 if (!this.enabled) 1332 { 1333 return false; 1334 } 1335 1336 if (!connectionDelay) 1337 { 1338 // never fallback to long polling 1339 // TODO remove long polling support later 1340 /*if (this.connectionAttempt > 3 && this.connectionType === ConnectionType.WebSocket && !this.sharedConfig.isLongPollingBlocked()) 1341 { 1342 // Websocket seems to be closed by network filter. Trying to fallback to long polling 1343 this.sharedConfig.setWebSocketBlocked(true); 1344 this.connectionType = ConnectionType.LongPolling; 1345 this.connectionAttempt = 1; 1346 connectionDelay = 1; 1347 } 1348 else*/ 1349 { 1350 connectionDelay = this.getConnectionAttemptDelay(this.connectionAttempt); 1351 } 1352 } 1353 if (this.reconnectTimeout) 1354 { 1355 clearTimeout(this.reconnectTimeout); 1356 } 1357 1358 this.logToConsole('Pull: scheduling reconnection in ' + connectionDelay + ' seconds; attempt # ' + this.connectionAttempt); 1359 1360 this.reconnectTimeout = setTimeout( 1361 () => { 1362 this.connect().catch(error => { 1363 console.error(error) 1364 }) 1365 }, 1366 connectionDelay * 1000); 1367 } 1368 1369 scheduleRestoreWebSocketConnection() 1370 { 1371 this.logToConsole('Pull: scheduling restoration of websocket connection in ' + RESTORE_WEBSOCKET_TIMEOUT + ' seconds'); 1372 1373 if (this.restoreWebSocketTimeout) 1374 { 1375 return; 1376 } 1377 1378 this.restoreWebSocketTimeout = setTimeout(() => { 1379 this.restoreWebSocketTimeout = 0; 1380 this.restoreWebSocketConnection(); 1381 }, RESTORE_WEBSOCKET_TIMEOUT * 1000); 1382 } 1383 1384 /** 1385 * @returns {Promise} 1386 */ 1387 connect() 1388 { 1389 if (!this.enabled) 1390 { 1391 return Promise.reject(); 1392 } 1393 if (this.connector.connected) 1394 { 1395 return Promise.resolve(); 1396 } 1397 1398 if (this.reconnectTimeout) 1399 { 1400 clearTimeout(this.reconnectTimeout); 1401 } 1402 1403 this.status = PullStatus.Connecting; 1404 this.connectionAttempt++; 1405 return new Promise((resolve, reject) => { 1406 this._connectPromise = {resolve, reject} 1407 this.connector.connect(); 1408 }) 1409 } 1410 1411 onIncomingMessage(message) 1412 { 1413 if (this.isJsonRpc()) 1414 { 1415 (message === JSON_RPC_PING) ? this.onJsonRpcPing() : this.jsonRpcAdapter.parseJsonRpcMessage(message); 1416 } 1417 else 1418 { 1419 const events = this.extractMessages(message); 1420 this.handleIncomingEvents(events); 1421 } 1422 } 1423 1424 handleRpcIncomingMessage(messageFields) 1425 { 1426 this.session.mid = messageFields.mid; 1427 let body = messageFields.body; 1428 1429 if (!messageFields.body.extra) 1430 { 1431 body.extra = {}; 1432 } 1433 body.extra.sender = messageFields.sender; 1434 1435 if ("user_params" in messageFields && Utils.isPlainObject(messageFields.user_params)) 1436 { 1437 Object.assign(body.params, messageFields.user_params) 1438 } 1439 1440 if ("dictionary" in messageFields && Utils.isPlainObject(messageFields.dictionary)) 1441 { 1442 Object.assign(body.params, messageFields.dictionary) 1443 } 1444 1445 if (this.checkDuplicate(messageFields.mid)) 1446 { 1447 this.addMessageToStat(body); 1448 this.trimDuplicates(); 1449 this.broadcastMessage(body) 1450 } 1451 1452 this.connector.send(`mack:${messageFields.mid}`) 1453 1454 return {}; 1455 } 1456 1457 onJsonRpcPing() 1458 { 1459 this.updatePingWaitTimeout(); 1460 this.connector.send(JSON_RPC_PONG) 1461 } 1462 1463 handleIncomingEvents(events) 1464 { 1465 let messages = []; 1466 if (events.length === 0) 1467 { 1468 this.session.mid = null; 1469 return; 1470 } 1471 1472 for (let i = 0; i < events.length; i++) 1473 { 1474 let event = events[i]; 1475 this.updateSessionFromEvent(event); 1476 if (event.mid && !this.checkDuplicate(event.mid)) 1477 { 1478 continue; 1479 } 1480 1481 this.addMessageToStat(event.text); 1482 messages.push(event.text); 1483 } 1484 this.trimDuplicates(); 1485 this.broadcastMessages(messages); 1486 } 1487 1488 updateSessionFromEvent(event) 1489 { 1490 this.session.mid = event.mid || null; 1491 this.session.tag = event.tag || null; 1492 this.session.time = event.time || null; 1493 } 1494 1495 checkDuplicate(mid) 1496 { 1497 if (this.session.lastMessageIds.includes(mid)) 1498 { 1499 console.warn("Duplicate message " + mid + " skipped"); 1500 return false;
1501 } 1502 else 1503 { 1504 this.session.lastMessageIds.push(mid); 1505 return true; 1506 } 1507 } 1508 1509 trimDuplicates() 1510 { 1511 if (this.session.lastMessageIds.length > MAX_IDS_TO_STORE) 1512 { 1513 this.session.lastMessageIds = this.session.lastMessageIds.slice(-MAX_IDS_TO_STORE); 1514 } 1515 } 1516 1517 addMessageToStat(message) 1518 { 1519 if (!this.session.history[message.module_id]) 1520 { 1521 this.session.history[message.module_id] = {}; 1522 } 1523 if (!this.session.history[message.module_id][message.command]) 1524 { 1525 this.session.history[message.module_id][message.command] = 0; 1526 } 1527 this.session.history[message.module_id][message.command]++; 1528 1529 this.session.messageCount++; 1530 } 1531 1532 extractMessages(pullEvent) 1533 { 1534 if (pullEvent instanceof ArrayBuffer) 1535 { 1536 return this.extractProtobufMessages(pullEvent); 1537 } 1538 else if (Utils.isNotEmptyString(pullEvent)) 1539 { 1540 return this.extractPlainTextMessages(pullEvent) 1541 } 1542 } 1543 1544 extractProtobufMessages(pullEvent) 1545 { 1546 let result = []; 1547 try 1548 { 1549 let responseBatch = ResponseBatch.decode(new Uint8Array(pullEvent)); 1550 for (let i = 0; i < responseBatch.responses.length; i++) 1551 { 1552 let response = responseBatch.responses[i]; 1553 if (response.command != "outgoingMessages") 1554 { 1555 continue; 1556 } 1557 1558 let messages = response.outgoingMessages.messages; 1559 for (let m = 0; m < messages.length; m++) 1560 { 1561 const message = messages[m]; 1562 let messageFields; 1563 try 1564 { 1565 messageFields = JSON.parse(message.body) 1566 } catch (e) 1567 { 1568 console.error(Utils.getDateForLog() + ": Pull: Could not parse message body", e); 1569 continue; 1570 } 1571 1572 if (!messageFields.extra) 1573 { 1574 messageFields.extra = {} 1575 } 1576 messageFields.extra.sender = { 1577 type: message.sender.type 1578 }; 1579 1580 if (message.sender.id instanceof Uint8Array) 1581 { 1582 messageFields.extra.sender.id = this.decodeId(message.sender.id) 1583 } 1584 1585 const compatibleMessage = { 1586 mid: this.decodeId(message.id), 1587 text: messageFields 1588 }; 1589 1590 result.push(compatibleMessage); 1591 } 1592 } 1593 } catch (e) 1594 { 1595 console.error(Utils.getDateForLog() + ": Pull: Could not parse message", e) 1596 } 1597 return result; 1598 } 1599 1600 extractPlainTextMessages(pullEvent) 1601 { 1602 let result = []; 1603 const dataArray = pullEvent.match(/#!NGINXNMS!#(.*?)#!NGINXNME!#/gm); 1604 if (dataArray === null) 1605 { 1606 const text = "\n========= PULL ERROR ===========\n" + 1607 "Error type: parseResponse error parsing message\n" + 1608 "\n" + 1609 "Data string: " + pullEvent + "\n" + 1610 "================================\n\n"; 1611 console.warn(text); 1612 return result; 1613 } 1614 for (let i = 0; i < dataArray.length; i++) 1615 { 1616 dataArray[i] = dataArray[i].substring(12, dataArray[i].length - 12); 1617 if (dataArray[i].length <= 0) 1618 { 1619 continue; 1620 } 1621 1622 let data 1623 try 1624 { 1625 data = JSON.parse(dataArray[i]) 1626 } catch (e) 1627 { 1628 continue; 1629 } 1630 1631 result.push(data); 1632 } 1633 return result; 1634 } 1635 1636 /** 1637 * Converts message id from byte[] to string 1638 * @param {Uint8Array} encodedId 1639 * @return {string} 1640 */ 1641 decodeId(encodedId) 1642 { 1643 if (!(encodedId instanceof Uint8Array)) 1644 { 1645 throw new Error("encodedId should be an instance of Uint8Array"); 1646 } 1647 1648 let result = ""; 1649 for (let i = 0; i < encodedId.length; i++) 1650 { 1651 const hexByte = encodedId[i].toString(16); 1652 if (hexByte.length === 1) 1653 { 1654 result += '0'; 1655 } 1656 result += hexByte; 1657 } 1658 return result; 1659 } 1660 1661 /** 1662 * Converts message id from hex-encoded string to byte[] 1663 * @param {string} id Hex-encoded string. 1664 * @return {Uint8Array} 1665 */ 1666 encodeId(id) 1667 { 1668 if (!id) 1669 { 1670 return new Uint8Array(); 1671 } 1672 1673 let result = []; 1674 for (let i = 0; i < id.length; i += 2) 1675 { 1676 result.push(parseInt(id.substr(i, 2), 16)); 1677 } 1678 1679 return new Uint8Array(result); 1680 } 1681 1682 broadcastMessages(messages) 1683 { 1684 messages.forEach(message => this.broadcastMessage(message)); 1685 } 1686 1687 broadcastMessage(message) 1688 { 1689 const moduleId = message.module_id = message.module_id.toLowerCase(); 1690 const command = message.command; 1691 1692 if (!message.extra) 1693 { 1694 message.extra = {}; 1695 } 1696 1697 if (message.extra.server_time_unix) 1698 { 1699 message.extra.server_time_ago = ((Utils.getTimestamp() - (message.extra.server_time_unix * 1000)) / 1000) - (this.config.server.timeShift ? this.config.server.timeShift : 0); 1700 message.extra.server_time_ago = message.extra.server_time_ago > 0 ? message.extra.server_time_ago : 0; 1701 } 1702 1703 this.logMessage(message); 1704 try 1705 { 1706 if (message.extra.sender && message.extra.sender.type === SenderType.Client) 1707 { 1708 if (typeof BX.onCustomEvent !== 'undefined') 1709 { 1710 BX.onCustomEvent(window, 'onPullClientEvent-' + moduleId, [command, message.params, message.extra], true); 1711 BX.onCustomEvent(window, 'onPullClientEvent', [moduleId, command, message.params, message.extra], true); 1712 } 1713
1714 this.emit({ 1715 type: SubscriptionType.Client, 1716 moduleId: moduleId, 1717 data: { 1718 command: command, 1719 params: Utils.clone(message.params), 1720 extra: Utils.clone(message.extra) 1721 } 1722 }); 1723 } 1724 else if (moduleId === 'pull') 1725 { 1726 this.handleInternalPullEvent(command, message); 1727 } 1728 else if (moduleId == 'online') 1729 { 1730 if (message.extra.server_time_ago < 240) 1731 { 1732 if (typeof BX.onCustomEvent !== 'undefined') 1733 { 1734 BX.onCustomEvent(window, 'onPullOnlineEvent', [command, message.params, message.extra], true); 1735 } 1736 1737 this.emit({ 1738 type: SubscriptionType.Online, 1739 data: { 1740 command: command, 1741 params: Utils.clone(message.params), 1742 extra: Utils.clone(message.extra) 1743 } 1744 }); 1745 } 1746 1747 if (command === 'userStatusChange') 1748 { 1749 this.emitUserStatusChange(message.params.user_id, message.params.online); 1750 } 1751 } 1752 else 1753 { 1754 if (typeof BX.onCustomEvent !== 'undefined') 1755 { 1756 BX.onCustomEvent(window, 'onPullEvent-' + moduleId, [command, message.params, message.extra], true); 1757 BX.onCustomEvent(window, 'onPullEvent', [moduleId, command, message.params, message.extra], true); 1758 } 1759 1760 this.emit({ 1761 type: SubscriptionType.Server, 1762 moduleId: moduleId, 1763 data: { 1764 command: command, 1765 params: Utils.clone(message.params), 1766 extra: Utils.clone(message.extra) 1767 } 1768 }); 1769 } 1770 } catch (e) 1771 { 1772 if (typeof (console) == 'object') 1773 { 1774 console.warn( 1775 "\n========= PULL ERROR ===========\n" + 1776 "Error type: broadcastMessages execute error\n" + 1777 "Error event: ", e, "\n" + 1778 "Message: ", message, "\n" + 1779 "================================\n" 1780 ); 1781 if (typeof BX.debug !== 'undefined') 1782 { 1783 BX.debug(e); 1784 } 1785 } 1786 } 1787 1788 if (message.extra && message.extra.revision_web) 1789 { 1790 this.checkRevision(message.extra.revision_web); 1791 } 1792 } 1793 1794 logToConsole(message, force) 1795 { 1796 if (this.loggingEnabled || force) 1797 { 1798 console.log(Utils.getDateForLog() + ': ' + message); 1799 } 1800 } 1801 1802 logMessage(message) 1803 { 1804 if (!this.debug) 1805 { 1806 return; 1807 } 1808 1809 if (message.extra.sender && message.extra.sender.type === SenderType.Client) 1810 { 1811 console.info('onPullClientEvent-' + message.module_id, message.command, message.params, message.extra); 1812 } 1813 else if (message.moduleId == 'online') 1814 { 1815 console.info('onPullOnlineEvent', message.command, message.params, message.extra); 1816 } 1817 else 1818 { 1819 console.info('onPullEvent', message.module_id, message.command, message.params, message.extra); 1820 } 1821 } 1822 1823 onLongPollingOpen() 1824 { 1825 this.unloading = false; 1826 this.starting = false; 1827 this.connectionAttempt = 0; 1828 this.isManualDisconnect = false; 1829 this.status = PullStatus.Online; 1830 1831 this.logToConsole('Pull: Long polling connection with push-server opened'); 1832 if (this.isWebSocketEnabled()) 1833 { 1834 this.scheduleRestoreWebSocketConnection(); 1835 } 1836 if (this._connectPromise) 1837 { 1838 this._connectPromise.resolve(); 1839 } 1840 } 1841 1842 onWebSocketBlockChanged(e) 1843 { 1844 const isWebSocketBlocked = e.isWebSocketBlocked; 1845 1846 if (isWebSocketBlocked && this.connectionType === ConnectionType.WebSocket && !this.isConnected()) 1847 { 1848 clearTimeout(this.reconnectTimeout); 1849 1850 this.connectionAttempt = 0; 1851 this.connectionType = ConnectionType.LongPolling; 1852 this.scheduleReconnect(1); 1853 } 1854 else if (!isWebSocketBlocked && this.connectionType === ConnectionType.LongPolling) 1855 { 1856 clearTimeout(this.reconnectTimeout); 1857 clearTimeout(this.restoreWebSocketTimeout); 1858 1859 this.connectionAttempt = 0; 1860 this.connectionType = ConnectionType.WebSocket; 1861 this.scheduleReconnect(1); 1862 } 1863 } 1864 1865 onWebSocketOpen() 1866 { 1867 this.unloading = false; 1868 this.starting = false; 1869 this.connectionAttempt = 0; 1870 this.isManualDisconnect = false; 1871 this.status = PullStatus.Online; 1872 this.sharedConfig.setWebSocketBlocked(false); 1873 1874 // to prevent fallback to long polling in case of networking problems 1875 this.sharedConfig.setLongPollingBlocked(true); 1876 1877 if (this.connectionType == ConnectionType.LongPolling) 1878 { 1879 this.connectionType = ConnectionType.WebSocket; 1880 this._connectors.longPolling.disconnect(); 1881 } 1882 1883 if (this.restoreWebSocketTimeout) 1884 { 1885 clearTimeout(this.restoreWebSocketTimeout); 1886 this.restoreWebSocketTimeout = null; 1887 } 1888 this.logToConsole('Pull: Websocket connection with push-server opened'); 1889 if (this._connectPromise) 1890 { 1891 this._connectPromise.resolve(); 1892 }
1893 this.restoreUserStatusSubscription(); 1894 } 1895 1896 onWebSocketDisconnect(e) 1897 { 1898 if (this.connectionType === ConnectionType.WebSocket) 1899 { 1900 this.status = PullStatus.Offline; 1901 } 1902 1903 if (!e) 1904 { 1905 e = {}; 1906 } 1907 1908 this.logToConsole('Pull: Websocket connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason, true); 1909 if (!this.isManualDisconnect) 1910 { 1911 if (e.code == CloseReasons.WRONG_CHANNEL_ID) 1912 { 1913 this.scheduleRestart(CloseReasons.WRONG_CHANNEL_ID, "wrong channel signature"); 1914 } 1915 else 1916 { 1917 this.scheduleReconnect(); 1918 } 1919 } 1920 1921 // to prevent fallback to long polling in case of networking problems 1922 this.sharedConfig.setLongPollingBlocked(true); 1923 this.isManualDisconnect = false; 1924 1925 this.clearPingWaitTimeout(); 1926 } 1927 1928 onWebSocketError(e) 1929 { 1930 this.starting = false; 1931 if (this.connectionType === ConnectionType.WebSocket) 1932 { 1933 this.status = PullStatus.Offline; 1934 } 1935 1936 console.error(Utils.getDateForLog() + ": Pull: WebSocket connection error", e); 1937 this.scheduleReconnect(); 1938 if (this._connectPromise) 1939 { 1940 this._connectPromise.reject(); 1941 } 1942 1943 this.clearPingWaitTimeout(); 1944 } 1945 1946 onLongPollingDisconnect(e) 1947 { 1948 if (this.connectionType === ConnectionType.LongPolling) 1949 { 1950 this.status = PullStatus.Offline; 1951 } 1952 1953 if (!e) 1954 { 1955 e = {}; 1956 } 1957 1958 this.logToConsole('Pull: Long polling connection with push-server closed. Code: ' + e.code + ', reason: ' + e.reason); 1959 if (!this.isManualDisconnect) 1960 { 1961 this.scheduleReconnect(); 1962 } 1963 this.isManualDisconnect = false; 1964 this.clearPingWaitTimeout(); 1965 } 1966 1967 onLongPollingError(e) 1968 { 1969 this.starting = false; 1970 if (this.connectionType === ConnectionType.LongPolling) 1971 { 1972 this.status = PullStatus.Offline; 1973 } 1974 console.error(Utils.getDateForLog() + ': Pull: Long polling connection error', e); 1975 this.scheduleReconnect(); 1976 if (this._connectPromise) 1977 { 1978 this._connectPromise.reject(); 1979 } 1980 this.clearPingWaitTimeout(); 1981 } 1982 1983 isConnected() 1984 { 1985 return this.connector ? this.connector.connected : false; 1986 } 1987 1988 onBeforeUnload() 1989 { 1990 this.unloading = true; 1991 1992 const session = Utils.clone(this.session); 1993 session.ttl = (new Date()).getTime() + LS_SESSION_CACHE_TIME * 1000; 1994 if (this.storage) 1995 { 1996 try 1997 { 1998 this.storage.set(LS_SESSION, JSON.stringify(session), LS_SESSION_CACHE_TIME); 1999 } catch (e) 2000 { 2001 console.error(Utils.getDateForLog() + " Pull: Could not save session info in local storage. Error: ", e); 2002 } 2003 } 2004 2005 this.scheduleReconnect(15); 2006 } 2007 2008 onOffline() 2009 { 2010 this.disconnect("1000", "offline"); 2011 } 2012 2013 onOnline() 2014 { 2015 this.connect(); 2016 } 2017 2018 handleInternalPullEvent(command, message) 2019 { 2020 switch (command.toUpperCase()) 2021 { 2022 case SystemCommands.CHANNEL_EXPIRE: 2023 { 2024 if (message.params.action == 'reconnect') 2025 { 2026 this.config.channels[message.params.channel.type] = message.params.new_channel; 2027 this.logToConsole("Pull: new config for " + message.params.channel.type + " channel set:\n", this.config.channels[message.params.channel.type]); 2028 2029 this.reconnect(CloseReasons.CONFIG_REPLACED, "config was replaced"); 2030 } 2031 else 2032 { 2033 this.restart(CloseReasons.CHANNEL_EXPIRED, "channel expired received"); 2034 } 2035 break; 2036 } 2037 case SystemCommands.CONFIG_EXPIRE: 2038 { 2039 this.restart(CloseReasons.CONFIG_EXPIRED, "config expired received"); 2040 break; 2041 } 2042 case SystemCommands.SERVER_RESTART: 2043 { 2044 this.reconnect(CloseReasons.SERVER_RESTARTED, "server was restarted", 15); 2045 break; 2046 } 2047 default:// 2048 } 2049 } 2050 2051 checkRevision(serverRevision) 2052 { 2053 if (this.skipCheckRevision) 2054 { 2055 return true; 2056 } 2057 2058 serverRevision = parseInt(serverRevision); 2059 if (serverRevision > 0 && serverRevision != REVISION) 2060 { 2061 this.enabled = false; 2062 if (typeof BX.message !== 'undefined') 2063 { 2064 this.showNotification(BX.message('PULL_OLD_REVISION')); 2065 } 2066 this.disconnect(CloseReasons.NORMAL_CLOSURE, 'check_revision'); 2067 2068 if (typeof BX.onCustomEvent !== 'undefined') 2069 { 2070 BX.onCustomEvent(window, 'onPullRevisionUp', [serverRevision, REVISION]); 2071 } 2072
2073 this.emit({ 2074 type: SubscriptionType.Revision, 2075 data: { 2076 server: serverRevision, 2077 client: REVISION 2078 } 2079 }); 2080 2081 this.logToConsole("Pull revision changed from " + REVISION + " to " + serverRevision + ". Reload required"); 2082 2083 return false; 2084 } 2085 return true; 2086 } 2087 2088 showNotification(text) 2089 { 2090 if (this.notificationPopup || typeof BX.PopupWindow === 'undefined') 2091 { 2092 return; 2093 } 2094 2095 this.notificationPopup = new BX.PopupWindow('bx-notifier-popup-confirm', null, { 2096 zIndex: 200, 2097 autoHide: false, 2098 closeByEsc: false, 2099 overlay: true, 2100 content: BX.create("div", { 2101 props: {className: "bx-messenger-confirm"}, 2102 html: text 2103 }), 2104 buttons: [ 2105 new BX.PopupWindowButton({ 2106 text: BX.message('JS_CORE_WINDOW_CLOSE'), 2107 className: "popup-window-button-decline", 2108 events: { 2109 click: () => this.notificationPopup.close(), 2110 } 2111 }) 2112 ], 2113 events: { 2114 onPopupClose: () => this.notificationPopup.destroy(), 2115 onPopupDestroy: () => this.notificationPopup = null, 2116 } 2117 }); 2118 this.notificationPopup.show(); 2119 } 2120 2121 getRevision() 2122 { 2123 return (this.config && this.config.api) ? this.config.api.revision_web : null; 2124 } 2125 2126 getServerVersion() 2127 { 2128 return (this.config && this.config.server) ? this.config.server.version : 0; 2129 } 2130 2131 getServerMode() 2132 { 2133 return (this.config && this.config.server) ? this.config.server.mode : null; 2134 } 2135 2136 getConfig() 2137 { 2138 return this.config; 2139 } 2140 2141 getDebugInfo() 2142 { 2143 if (!JSON || !JSON.stringify) 2144 { 2145 return false; 2146 } 2147 2148 let configDump; 2149 if (this.config && this.config.channels) 2150 { 2151 configDump = { 2152 "ChannelID": (this.config.channels.private ? this.config.channels.private.id : "n/a"), 2153 "ChannelDie": (this.config.channels.private ? this.config.channels.private.end : "n/a"), 2154 "ChannelDieShared": ("shared" in this.config.channels ? this.config.channels.shared.end : "n/a"), 2155 }; 2156 } 2157 else 2158 { 2159 configDump = {"Config error": "config is not loaded"} 2160 } 2161 2162 let websocketMode = "-"; 2163 if (this._connectors.webSocket && this._connectors.webSocket.socket) 2164 { 2165 if (this.isJsonRpc()) 2166 { 2167 websocketMode = "json-rpc" 2168 } 2169 else 2170 { 2171 websocketMode = (this._connectors.webSocket.socket.url.search("binaryMode=true") != -1 ? "protobuf" : "text") 2172 } 2173 } 2174 2175 return { 2176 "UserId": this.userId + (this.userId > 0 ? '' : '(guest)'), 2177 "Guest userId": (this.guestMode && this.guestUserId !== 0 ? this.guestUserId : "-"), 2178 "Browser online": (navigator.onLine ? 'Y' : 'N'), 2179 "Connect": (this.isConnected() ? 'Y' : 'N'), 2180 "Server type": (this.isSharedMode() ? 'cloud' : 'local'), 2181 "WebSocket supported": (this.isWebSocketSupported() ? 'Y' : 'N'), 2182 "WebSocket connected": (this._connectors.webSocket && this._connectors.webSocket.connected ? 'Y' : 'N'), 2183 "WebSocket mode": websocketMode, 2184 2185 "Try connect": (this.reconnectTimeout ? 'Y' : 'N'), 2186 "Try number": (this.connectionAttempt), 2187 2188 "Path": (this.connector ? this.connector.path : '-'), 2189 ...configDump, 2190 2191 "Last message": (this.session.mid > 0 ? this.session.mid : '-'), 2192 "Session history": this.session.history, 2193 "Watch tags": this.watchTagsQueue, 2194 } 2195 } 2196 2197 enableLogging(loggingFlag) 2198 { 2199 if (loggingFlag === undefined) 2200 { 2201 loggingFlag = true; 2202 } 2203 loggingFlag = loggingFlag === true; 2204 2205 this.sharedConfig.setLoggingEnabled(loggingFlag); 2206 this.loggingEnabled = loggingFlag; 2207 } 2208 2209 capturePullEvent(debugFlag) 2210 { 2211 if (debugFlag === undefined) 2212 { 2213 debugFlag = true; 2214 } 2215 2216 this.debug = debugFlag; 2217 } 2218 2219 getConnectionPath(connectionType) 2220 { 2221 let path; 2222 let params = {}; 2223 2224 switch (connectionType) 2225 { 2226 case ConnectionType.WebSocket: 2227 path = this.isSecure ? this.config.server.websocket_secure : this.config.server.websocket; 2228 break; 2229 case ConnectionType.LongPolling: 2230 path = this.isSecure ? this.config.server.long_pooling_secure : this.config.server.long_polling; 2231 break; 2232 default: 2233 throw new Error("Unknown connection type " + connectionType); 2234 } 2235 2236 if (!Utils.isNotEmptyString(path)) 2237 { 2238 return false;
2239 } 2240 2241 if (typeof (this.config.jwt) == 'string' && this.config.jwt !== '') 2242 { 2243 params['token'] = this.config.jwt; 2244 } 2245 else 2246 { 2247 let channels = []; 2248 ['private', 'shared'].forEach((type) => { 2249 if (typeof this.config.channels[type] !== 'undefined') 2250 { 2251 channels.push(this.config.channels[type].id); 2252 } 2253 }); 2254 if (channels.length === 0) 2255 { 2256 return false; 2257 } 2258 2259 params['CHANNEL_ID'] = channels.join('/'); 2260 } 2261 2262 if (this.isJsonRpc()) 2263 { 2264 params.jsonRpc = 'true'; 2265 } 2266 else if (this.isProtobufSupported()) 2267 { 2268 params.binaryMode = 'true'; 2269 } 2270 2271 if (this.isSharedMode()) 2272 { 2273 if (!this.config.clientId) 2274 { 2275 throw new Error("Push-server is in shared mode, but clientId is not set"); 2276 } 2277 params.clientId = this.config.clientId; 2278 } 2279 if (this.session.mid) 2280 { 2281 params.mid = this.session.mid; 2282 } 2283 if (this.session.tag) 2284 { 2285 params.tag = this.session.tag; 2286 } 2287 if (this.session.time) 2288 { 2289 params.time = this.session.time; 2290 } 2291 params.revision = REVISION; 2292 2293 return path + '?' + Utils.buildQueryString(params); 2294 } 2295 2296 getPublicationPath() 2297 { 2298 const path = this.isSecure ? this.config.server.publish_secure : this.config.server.publish; 2299 if (!path) 2300 { 2301 return ''; 2302 } 2303 2304 let channels = []; 2305 for (let type in this.config.channels) 2306 { 2307 if (!this.config.channels.hasOwnProperty(type)) 2308 { 2309 continue; 2310 } 2311 channels.push(this.config.channels[type].id); 2312 } 2313 2314 const params = { 2315 CHANNEL_ID: channels.join('/') 2316 }; 2317 2318 return path + '?' + Utils.buildQueryString(params); 2319 } 2320 2321 /** 2322 * Returns reconnect delay in seconds 2323 * @param attemptNumber 2324 * @return {number} 2325 */ 2326 getConnectionAttemptDelay(attemptNumber) 2327 { 2328 let result; 2329 if (attemptNumber < 1) 2330 { 2331 result = 0.5; 2332 } 2333 else if (attemptNumber < 3) 2334 { 2335 result = 15; 2336 } 2337 else if (attemptNumber < 5) 2338 { 2339 result = 45; 2340 } 2341 else if (attemptNumber < 10) 2342 { 2343 result = 600; 2344 } 2345 else 2346 { 2347 result = 3600; 2348 } 2349 2350 return result + (result * Math.random() * 0.2); 2351 } 2352 2353 sendPullStatusDelayed(status, delay) 2354 { 2355 if (this.offlineTimeout) 2356 { 2357 clearTimeout(this.offlineTimeout) 2358 } 2359 this.offlineTimeout = setTimeout( 2360 () => { 2361 this.offlineTimeout = null; 2362 this.sendPullStatus(status); 2363 }, 2364 delay 2365 ) 2366 } 2367 2368 sendPullStatus(status) 2369 { 2370 if (this.unloading) 2371 { 2372 return; 2373 } 2374 2375 if (typeof BX.onCustomEvent !== 'undefined') 2376 { 2377 BX.onCustomEvent(window, 'onPullStatus', [status]); 2378 } 2379 2380 this.emit({ 2381 type: SubscriptionType.Status, 2382 data: { 2383 status: status 2384 } 2385 }); 2386 } 2387 2388 extendWatch(tag, force) 2389 { 2390 if (!tag || this.watchTagsQueue[tag]) 2391 { 2392 return false; 2393 } 2394 2395 this.watchTagsQueue[tag] = true; 2396 if (force) 2397 { 2398 this.updateWatch(force); 2399 } 2400 } 2401 2402 updateWatch(force) 2403 { 2404 clearTimeout(this.watchUpdateTimeout); 2405 this.watchUpdateTimeout = setTimeout(() => { 2406 const watchTags = Object.keys(this.watchTagsQueue); 2407 if (watchTags.length > 0) 2408 { 2409 this.restClient.callMethod('pull.watch.extend', {tags: watchTags}, (result) => { 2410 if (result.error()) 2411 { 2412 this.updateWatch(); 2413 2414 return false; 2415 } 2416 2417 const updatedTags = result.data(); 2418 2419 for (let tagId in updatedTags) 2420 { 2421 if (updatedTags.hasOwnProperty(tagId) && !updatedTags[tagId]) 2422 { 2423 this.clearWatch(tagId); 2424 } 2425 } 2426 this.updateWatch(); 2427 }) 2428 } 2429 else 2430 { 2431 this.updateWatch(); 2432 } 2433 }, force ? this.watchForceUpdateInterval : this.watchUpdateInterval); 2434 } 2435 2436 clearWatch(tagId) 2437 { 2438 delete this.watchTagsQueue[tagId]; 2439 } 2440 2441 updatePingWaitTimeout() 2442 { 2443 clearTimeout(this.pingWaitTimeout); 2444 this.pingWaitTimeout = setTimeout(this.onPingTimeoutHandler, PING_TIMEOUT * 2 * 1000) 2445 } 2446 2447 clearPingWaitTimeout() 2448 { 2449 clearTimeout(this.pingWaitTimeout); 2450 this.pingWaitTimeout = null; 2451 } 2452 2453 onPingTimeout() 2454 { 2455 this.pingWaitTimeout = null; 2456 if (!this.enabled || !this.isConnected()) 2457 { 2458 return; 2459 } 2460 2461 console.warn("No pings are received in " + PING_TIMEOUT * 2 + " seconds. Reconnecting")
2462 this.disconnect(CloseReasons.STUCK, "connection stuck"); 2463 this.scheduleReconnect(); 2464 } 2465 2466 // old functions, not used anymore. 2467 setPrivateVar() {} 2468 2469 returnPrivateVar() {} 2470 2471 expireConfig() {} 2472 2473 updateChannelID() {} 2474 2475 tryConnect() {} 2476 2477 tryConnectDelay() {} 2478 2479 tryConnectSet() {} 2480 2481 updateState() {} 2482 2483 setUpdateStateStepCount() {} 2484 2485 supportWebSocket() 2486 { 2487 return this.isWebSocketSupported(); 2488 } 2489 2490 isWebSoketConnected() 2491 { 2492 return this.isConnected() && this.connectionType == ConnectionType.WebSocket; 2493 } 2494 2495 getPullServerStatus() {return this.isConnected()} 2496 2497 closeConfirm() 2498 { 2499 if (this.notificationPopup) 2500 { 2501 this.notificationPopup.destroy(); 2502 } 2503 } 2504 } 2505 2506 class SharedConfig 2507 { 2508 constructor(params) 2509 { 2510 params = params || {}; 2511 this.storage = params.storage || new StorageManager(); 2512 2513 this.ttl = 24 * 60 * 60; 2514 2515 this.lsKeys = { 2516 websocketBlocked: 'bx-pull-websocket-blocked', 2517 longPollingBlocked: 'bx-pull-longpolling-blocked', 2518 loggingEnabled: 'bx-pull-logging-enabled' 2519 }; 2520 2521 this.callbacks = { 2522 onWebSocketBlockChanged: (Utils.isFunction(params.onWebSocketBlockChanged) ? params.onWebSocketBlockChanged : function () {}) 2523 }; 2524 2525 if (this.storage) 2526 { 2527 window.addEventListener('storage', this.onLocalStorageSet.bind(this)); 2528 } 2529 } 2530 2531 onLocalStorageSet(params) 2532 { 2533 if ( 2534 this.storage.compareKey(params.key, this.lsKeys.websocketBlocked) 2535 && params.newValue != params.oldValue 2536 ) 2537 { 2538 this.callbacks.onWebSocketBlockChanged({ 2539 isWebSocketBlocked: this.isWebSocketBlocked() 2540 }) 2541 } 2542 } 2543 2544 isWebSocketBlocked() 2545 { 2546 if (!this.storage) 2547 { 2548 return false; 2549 } 2550 2551 return this.storage.get(this.lsKeys.websocketBlocked, 0) > Utils.getTimestamp(); 2552 } 2553 2554 setWebSocketBlocked(isWebSocketBlocked) 2555 { 2556 if (!this.storage) 2557 { 2558 return false; 2559 } 2560 2561 try 2562 { 2563 this.storage.set(this.lsKeys.websocketBlocked, (isWebSocketBlocked ? Utils.getTimestamp() + this.ttl : 0)); 2564 } catch (e) 2565 { 2566 console.error(Utils.getDateForLog() + " Pull: Could not save WS_blocked flag in local storage. Error: ", e); 2567 } 2568 } 2569 2570 isLongPollingBlocked() 2571 { 2572 if (!this.storage) 2573 { 2574 return false; 2575 } 2576 2577 return this.storage.get(this.lsKeys.longPollingBlocked, 0) > Utils.getTimestamp(); 2578 } 2579 2580 setLongPollingBlocked(isLongPollingBlocked) 2581 { 2582 if (!this.storage) 2583 { 2584 return false; 2585 } 2586 2587 try 2588 { 2589 this.storage.set(this.lsKeys.longPollingBlocked, (isLongPollingBlocked ? Utils.getTimestamp() + this.ttl : 0)); 2590 } catch (e) 2591 { 2592 console.error(Utils.getDateForLog() + " Pull: Could not save LP_blocked flag in local storage. Error: ", e); 2593 } 2594 } 2595 2596 isLoggingEnabled() 2597 { 2598 if (!this.storage) 2599 { 2600 return false; 2601 } 2602 2603 return this.storage.get(this.lsKeys.loggingEnabled, 0) > Utils.getTimestamp(); 2604 } 2605 2606 setLoggingEnabled(isLoggingEnabled) 2607 { 2608 if (!this.storage) 2609 { 2610 return false; 2611 } 2612 2613 try 2614 { 2615 this.storage.set(this.lsKeys.loggingEnabled, (isLoggingEnabled ? Utils.getTimestamp() + this.ttl : 0)); 2616 } catch (e) 2617 { 2618 console.error("LocalStorage error: ", e); 2619 return false; 2620 } 2621 } 2622 } 2623 2624 class AbstractConnector 2625 { 2626 _connected = false; 2627 connectionType = ""; 2628 2629 disconnectCode = ''; 2630 disconnectReason = ''; 2631 2632 constructor(config) 2633 { 2634 this.parent = config.parent; 2635 this.callbacks = { 2636 onOpen: Utils.isFunction(config.onOpen) ? config.onOpen : function () {}, 2637 onDisconnect: Utils.isFunction(config.onDisconnect) ? config.onDisconnect : function () {}, 2638 onError: Utils.isFunction(config.onError) ? config.onError : function () {}, 2639 onMessage: Utils.isFunction(config.onMessage) ? config.onMessage : function () {} 2640 }; 2641 } 2642 2643 get connected() 2644 { 2645 return this._connected 2646 } 2647 2648 set connected(value) 2649 { 2650 if (value == this._connected) 2651 { 2652 return; 2653 } 2654 2655 this._connected = value; 2656 2657 if (this._connected) 2658 { 2659 this.callbacks.onOpen(); 2660 } 2661 else 2662 { 2663 this.callbacks.onDisconnect({ 2664 code: this.disconnectCode, 2665 reason: this.disconnectReason 2666 }); 2667 } 2668 } 2669 2670 get path() 2671 { 2672 return this.parent.getConnectionPath(this.connectionType); 2673 } 2674 } 2675 2676 class WebSocketConnector extends AbstractConnector 2677 { 2678 constructor(config) 2679 { 2680 super(config) 2681 this.connectionType = ConnectionType.WebSocket; 2682 this.socket = null; 2683 2684 this.onSocketOpenHandler = this.onSocketOpen.bind(this); 2685 this.onSocketCloseHandler = this.onSocketClose.bind(this); 2686 this.onSocketErrorHandler = this.onSocketError.bind(this); 2687 this.onSocketMessageHandler = this.onSocketMessage.bind(this); 2688 } 2689 2690 connect() 2691 { 2692 if (this.socket) 2693 { 2694 if (this.socket.readyState === 1) 2695 { 2696 // already connected 2697 return true; 2698 } 2699 else 2700 { 2701 this.socket.removeEventListener('open', this.onSocketOpenHandler); 2702 this.socket.removeEventListener('close', this.onSocketCloseHandler); 2703 this.socket.removeEventListener('error', this.onSocketErrorHandler); 2704 this.socket.removeEventListener('message', this.onSocketMessageHandler); 2705 2706 this.socket.close(); 2707 this.socket = null; 2708 } 2709 } 2710 2711 this.createSocket(); 2712 } 2713 2714 disconnect(code, message) 2715 { 2716 if (this.socket !== null) 2717 { 2718 this.socket.removeEventListener('open', this.onSocketOpenHandler); 2719 this.socket.removeEventListener('close', this.onSocketCloseHandler); 2720 this.socket.removeEventListener('error', this.onSocketErrorHandler); 2721 this.socket.removeEventListener('message', this.onSocketMessageHandler); 2722 2723 this.socket.close(code, message); 2724 } 2725 this.socket = null; 2726 this.disconnectCode = code; 2727 this.disconnectReason = message; 2728 this.connected = false;
2729 } 2730 2731 createSocket() 2732 { 2733 if (this.socket) 2734 { 2735 throw new Error("Socket already exists"); 2736 } 2737 2738 if (!this.path) 2739 { 2740 throw new Error("Websocket connection path is not defined"); 2741 } 2742 2743 this.socket = new WebSocket(this.path); 2744 this.socket.binaryType = 'arraybuffer'; 2745 2746 this.socket.addEventListener('open', this.onSocketOpenHandler); 2747 this.socket.addEventListener('close', this.onSocketCloseHandler); 2748 this.socket.addEventListener('error', this.onSocketErrorHandler); 2749 this.socket.addEventListener('message', this.onSocketMessageHandler); 2750 } 2751 2752 /** 2753 * Sends some data to the server via websocket connection. 2754 * @param {ArrayBuffer} buffer Data to send. 2755 * @return {boolean} 2756 */ 2757 send(buffer) 2758 { 2759 if (!this.socket || this.socket.readyState !== 1) 2760 { 2761 console.error(Utils.getDateForLog() + ": Pull: WebSocket is not connected"); 2762 return false; 2763 } 2764 2765 this.socket.send(buffer); 2766 return true; 2767 } 2768 2769 onSocketOpen() 2770 { 2771 this.connected = true; 2772 } 2773 2774 onSocketClose(e) 2775 { 2776 this.socket = null; 2777 this.disconnectCode = e.code; 2778 this.disconnectReason = e.reason; 2779 this.connected = false; 2780 } 2781 2782 onSocketError(e) 2783 { 2784 this.callbacks.onError(e); 2785 } 2786 2787 onSocketMessage(e) 2788 { 2789 this.callbacks.onMessage(e.data); 2790 } 2791 2792 destroy() 2793 { 2794 if (this.socket) 2795 { 2796 this.socket.close(); 2797 this.socket = null; 2798 } 2799 } 2800 } 2801
2802 class LongPollingConnector extends AbstractConnector 2803 { 2804 constructor(config) 2805 { 2806 super(config); 2807 2808 this.active = false; 2809 this.connectionType = ConnectionType.LongPolling; 2810 this.requestTimeout = null; 2811 this.failureTimeout = null; 2812 this.xhr = this.createXhr(); 2813 this.requestAborted = false; 2814 } 2815 2816 createXhr() 2817 { 2818 const result = new XMLHttpRequest(); 2819 if (this.parent.isProtobufSupported() && !this.parent.isJsonRpc()) 2820 { 2821 result.responseType = "arraybuffer"; 2822 } 2823 result.addEventListener("readystatechange", this.onXhrReadyStateChange.bind(this)); 2824 return result; 2825 } 2826 2827 connect() 2828 { 2829 this.active = true; 2830 this.performRequest(); 2831 } 2832 2833 disconnect(code, reason) 2834 { 2835 this.active = false; 2836 2837 if (this.failureTimeout) 2838 { 2839 clearTimeout(this.failureTimeout); 2840 this.failureTimeout = null; 2841 } 2842 if (this.requestTimeout) 2843 { 2844 clearTimeout(this.requestTimeout); 2845 this.requestTimeout = null; 2846 } 2847 2848 if (this.xhr) 2849 { 2850 this.requestAborted = true; 2851 this.xhr.abort(); 2852 } 2853 2854 this.disconnectCode = code; 2855 this.disconnectReason = reason; 2856 this.connected = false; 2857 } 2858 2859 performRequest() 2860 { 2861 if (!this.active) 2862 { 2863 return; 2864 } 2865 2866 if (!this.path) 2867 { 2868 throw new Error("Long polling connection path is not defined"); 2869 } 2870 if (this.xhr.readyState !== 0 && this.xhr.readyState !== 4) 2871 { 2872 return; 2873 } 2874 2875 clearTimeout(this.failureTimeout); 2876 clearTimeout(this.requestTimeout); 2877 2878 this.failureTimeout = setTimeout(() => { this.connected = true }, 5000); 2879 this.requestTimeout = setTimeout(this.onRequestTimeout.bind(this), LONG_POLLING_TIMEOUT * 1000); 2880 2881 this.xhr.open("GET", this.path); 2882 this.xhr.send(); 2883 } 2884 2885 onRequestTimeout() 2886 { 2887 this.requestAborted = true; 2888 this.xhr.abort(); 2889 this.performRequest(); 2890 } 2891 2892 onXhrReadyStateChange() 2893 { 2894 if (this.xhr.readyState === 4) 2895 { 2896 if (!this.requestAborted || this.xhr.status == 200) 2897 { 2898 this.onResponse(this.xhr.response); 2899 } 2900 this.requestAborted = false; 2901 } 2902 } 2903 2904 /** 2905 * Sends some data to the server via http request. 2906 * @param {ArrayBuffer} buffer Data to send. 2907 * @return {bool} 2908 */ 2909 send(buffer) 2910 { 2911 const path = this.parent.getPublicationPath(); 2912 if (!path) 2913 { 2914 console.error(Utils.getDateForLog() + ": Pull: publication path is empty"); 2915 return false; 2916 } 2917 2918 let xhr = new XMLHttpRequest(); 2919 xhr.open("POST", path); 2920 xhr.send(buffer); 2921 } 2922 2923 onResponse(response) 2924 { 2925 if (this.failureTimeout) 2926 { 2927 clearTimeout(this.failureTimeout); 2928 this.failureTimeout = 0; 2929 } 2930 if (this.requestTimeout) 2931 { 2932 clearTimeout(this.requestTimeout); 2933 this.requestTimeout = 0; 2934 } 2935 2936 if (this.xhr.status == 200) 2937 { 2938 this.connected = true; 2939 if (Utils.isNotEmptyString(response) || (response instanceof ArrayBuffer)) 2940 { 2941 this.callbacks.onMessage(response); 2942 } 2943 else 2944 { 2945 this.parent.session.mid = null; 2946 } 2947 this.performRequest(); 2948 } 2949 else if (this.xhr.status == 304) 2950 { 2951 this.connected = true; 2952 if (this.xhr.getResponseHeader("Expires") === "Thu, 01 Jan 1973 11:11:01 GMT") 2953 { 2954 const lastMessageId = this.xhr.getResponseHeader("Last-Message-Id"); 2955 if (Utils.isNotEmptyString(lastMessageId)) 2956 { 2957 this.parent.setLastMessageId(lastMessageId); 2958 } 2959 } 2960 this.performRequest(); 2961 } 2962 else 2963 { 2964 this.callbacks.onError('Could not connect to the server'); 2965 this.connected = false; 2966 } 2967 } 2968 } 2969 2970 class ChannelManager 2971 { 2972 constructor(params) 2973 { 2974 this.publicIds = {}; 2975 2976 this.restClient = typeof params.restClient !== "undefined" ? params.restClient : BX.rest; 2977 2978 this.getPublicListMethod = params.getPublicListMethod; 2979 } 2980 2981 /** 2982 * 2983 * @param {Array} users Array of user ids. 2984 * @return {Promise} 2985 */ 2986 getPublicIds(users) 2987 { 2988 const now = new Date(); 2989 let result = {}; 2990 let unknownUsers = []; 2991 2992 for (let i = 0; i < users.length; i++) 2993 { 2994 const userId = users[i]; 2995 if (this.publicIds[userId] && this.publicIds[userId]['end'] > now) 2996 { 2997 result[userId] = this.publicIds[userId]; 2998 } 2999 else 3000 { 3001 unknownUsers.push(userId); 3002 } 3003 } 3004 3005 if (unknownUsers.length === 0) 3006 { 3007 return Promise.resolve(result); 3008 } 3009 3010 return new Promise((resolve) => { 3011 this.restClient.callMethod(this.getPublicListMethod, {users: unknownUsers}).then((response) => { 3012 if (response.error()) 3013 { 3014 return resolve({}); 3015 } 3016 3017 const data = response.data(); 3018 this.setPublicIds(Utils.objectValues(data));
3019 unknownUsers.forEach((userId) => { 3020 result[userId] = this.publicIds[userId]; 3021 }); 3022 3023 resolve(result); 3024 }); 3025 }) 3026 } 3027 3028 /** 3029 * 3030 * @param {object[]} publicIds 3031 * @param {integer} publicIds.user_id 3032 * @param {string} publicIds.public_id 3033 * @param {string} publicIds.signature 3034 * @param {Date} publicIds.start 3035 * @param {Date} publicIds.end 3036 */ 3037 setPublicIds(publicIds) 3038 { 3039 for (let i = 0; i < publicIds.length; i++) 3040 { 3041 const publicIdDescriptor = publicIds[i]; 3042 const userId = publicIdDescriptor.user_id; 3043 this.publicIds[userId] = { 3044 userId: userId, 3045 publicId: publicIdDescriptor.public_id, 3046 signature: publicIdDescriptor.signature, 3047 start: new Date(publicIdDescriptor.start), 3048 end: new Date(publicIdDescriptor.end) 3049 } 3050 } 3051 }; 3052 } 3053 3054 class StorageManager 3055 { 3056 constructor(params) 3057 { 3058 params = params || {}; 3059 3060 this.userId = params.userId ? params.userId : (typeof BX.message !== 'undefined' && BX.message.USER_ID ? BX.message.USER_ID : 0); 3061 this.siteId = params.siteId ? params.siteId : (typeof BX.message !== 'undefined' && BX.message.SITE_ID ? BX.message.SITE_ID : 'none'); 3062 } 3063 3064 set(name, value) 3065 { 3066 if (typeof window.localStorage === 'undefined') 3067 { 3068 return false; 3069 } 3070 if (typeof value != 'string') 3071 { 3072 if (value) 3073 { 3074 value = JSON.stringify(value); 3075 } 3076 } 3077 return window.localStorage.setItem(this.getKey(name), value) 3078 } 3079 3080 get(name, defaultValue) 3081 { 3082 if (typeof window.localStorage === 'undefined') 3083 { 3084 return defaultValue || null; 3085 } 3086 3087 const result = window.localStorage.getItem(this.getKey(name)); 3088 if (result === null) 3089 { 3090 return defaultValue || null; 3091 } 3092 3093 return JSON.parse(result); 3094 } 3095 3096 remove(name) 3097 { 3098 if (typeof window.localStorage === 'undefined') 3099 { 3100 return false; 3101 } 3102 return window.localStorage.removeItem(this.getKey(name)); 3103 } 3104 3105 getKey(name) 3106 { 3107 return 'bx-pull-' + this.userId + '-' + this.siteId + '-' + name; 3108 } 3109 3110 compareKey(eventKey, userKey) 3111 {
3112 return eventKey === this.getKey(userKey); 3113 } 3114 } 3115 3116 class JsonRpc 3117 { 3118 idCounter = 0; 3119 3120 handlers = {}; 3121 rpcResponseAwaiters = new Map(); 3122 3123 constructor(options) 3124 { 3125 this.connector = options.connector; 3126 if (Utils.isPlainObject(options.handlers)) 3127 { 3128 for (let method in options.handlers) 3129 { 3130 this.handle(method, options.handlers[method]); 3131 } 3132 } 3133 } 3134 3135 /** 3136 * @param {string} method 3137 * @param {function} handler 3138 */ 3139 handle(method, handler) 3140 { 3141 this.handlers[method] = handler; 3142 } 3143 3144 /** 3145 * Sends RPC command to the server. 3146 * 3147 * @param {string} method Method name 3148 * @param {object} params 3149 * @param {int} timeout 3150 * @returns {Promise} 3151 */ 3152 executeOutgoingRpcCommand(method, params, timeout) 3153 { 3154 if (!timeout) 3155 { 3156 timeout = 5; 3157 } 3158 return new Promise((resolve, reject) => { 3159 const request = this.createRequest(method, params); 3160 3161 if (!this.connector.send(JSON.stringify(request))) 3162 { 3163 reject(new ErrorNotConnected('websocket is not connected')); 3164 } 3165 3166 const t = setTimeout(() => { 3167 this.rpcResponseAwaiters.delete(request.id); 3168 reject(new ErrorTimeout('no response')); 3169 }, timeout * 1000); 3170 this.rpcResponseAwaiters.set(request.id, {resolve, reject, timeout: t}); 3171 }) 3172 } 3173 3174 /** 3175 * Executes array or rpc commands. Returns array of promises, each promise will be resolved individually. 3176 * 3177 * @param {JsonRpcRequest[]} batch 3178 * @returns {Promise[]} 3179 */ 3180 executeOutgoingRpcBatch(batch) 3181 { 3182 let requests = []; 3183 let promises = []; 3184 batch.forEach(({method, params, id}) => { 3185 const request = this.createRequest(method, params, id); 3186 requests.push(request); 3187 promises.push(new Promise((resolve, reject) => this.rpcResponseAwaiters.set(request.id, { 3188 resolve, 3189 reject 3190 }))); 3191 }); 3192 3193 this.connector.send(JSON.stringify(requests)); 3194 return promises; 3195 } 3196 3197 processRpcResponse(response) 3198 { 3199 if ("id" in response && this.rpcResponseAwaiters.has(response.id)) 3200 { 3201 const awaiter = this.rpcResponseAwaiters.get(response.id) 3202 if ("result" in response) 3203 { 3204 awaiter.resolve(response.result) 3205 } 3206 else if ("error" in response) 3207 { 3208 awaiter.reject(response.error) 3209 } 3210 else 3211 { 3212 awaiter.reject(new Error("wrong response structure")) 3213 } 3214 3215 clearTimeout(awaiter.timeout) 3216 this.rpcResponseAwaiters.delete(response.id) 3217 } 3218 else 3219 { 3220 console.error("Received rpc response with unknown id", response) 3221 } 3222 } 3223 3224 parseJsonRpcMessage(message) 3225 { 3226 let decoded 3227 try 3228 { 3229 decoded = JSON.parse(message); 3230 } catch (e) 3231 { 3232 console.error(Utils.getDateForLog() + ": Pull: Could not decode json rpc message", e); 3233 } 3234 3235 if (Utils.isArray(decoded)) 3236 { 3237 return this.executeIncomingRpcBatch(decoded); 3238 } 3239 else if (Utils.isJsonRpcRequest(decoded)) 3240 { 3241 return this.executeIncomingRpcCommand(decoded); 3242 } 3243 else if (Utils.isJsonRpcResponse(decoded)) 3244 { 3245 return this.processRpcResponse(decoded); 3246 } 3247 else 3248 { 3249 console.error(Utils.getDateForLog() + ": Pull: unknown rpc packet", decoded); 3250 } 3251 } 3252 3253 /** 3254 * Executes RPC command, received from the server 3255 * 3256 * @param {string} method 3257 * @param {object} params 3258 * @returns {object} 3259 */ 3260 executeIncomingRpcCommand({method, params}) 3261 { 3262 if (method in this.handlers) 3263 { 3264 return this.handlers[method].call(this, params) 3265 } 3266 3267 return { 3268 "error": RpcError.MethodNotFound 3269 } 3270 } 3271 3272 executeIncomingRpcBatch(batch) 3273 { 3274 let result = []; 3275 for (let command of batch) 3276 { 3277 if ("jsonrpc" in command) 3278 { 3279 if ("method" in command) 3280 { 3281 let commandResult = this.executeIncomingRpcCommand(command) 3282 if (commandResult) 3283 { 3284 commandResult["jsonrpc"] = JSON_RPC_VERSION; 3285 commandResult["id"] = command["id"]; 3286 3287 result.push(commandResult) 3288 } 3289 } 3290 else 3291 { 3292 this.processRpcResponse(command) 3293 } 3294 } 3295 else 3296 { 3297 console.error(Utils.getDateForLog() + ": Pull: unknown rpc command in batch", command); 3298 result.push({ 3299 "jsonrpc": "2.0", 3300 "error": RpcError.InvalidRequest, 3301 }) 3302 } 3303 } 3304 3305 return result; 3306 } 3307 3308 nextId() 3309 { 3310 return ++this.idCounter; 3311 } 3312 3313 createPublishRequest(messageBatch) 3314 { 3315 let result = messageBatch.map(message => this.createRequest('publish', message)); 3316 3317 if (result.length === 0) 3318 { 3319 return result[0] 3320 } 3321 3322 return result; 3323 } 3324 3325 createRequest(method, params, id) 3326 { 3327 if (!id) 3328 { 3329 id = this.nextId() 3330 } 3331 3332 return { 3333 jsonrpc: JSON_RPC_VERSION, 3334 method: method, 3335 params: params, 3336 id: id 3337 } 3338 } 3339 } 3340
3341 class ErrorNotConnected extends Error 3342 { 3343 constructor(message) 3344 { 3345 super(message); 3346 this.name = 'ErrorNotConnected'; 3347 } 3348 } 3349 3350 class ErrorTimeout extends Error 3351 { 3352 constructor(message) 3353 { 3354 super(message); 3355 this.name = 'ErrorTimeout'; 3356 } 3357 } 3358 3359 const Utils = { 3360 browser: { 3361 IsChrome: function () { 3362 return navigator.userAgent.toLowerCase().indexOf('chrome') != -1; 3363 }, 3364 IsFirefox: function () { 3365 return navigator.userAgent.toLowerCase().indexOf('firefox') != -1; 3366 }, 3367 IsIe: function () { 3368 return navigator.userAgent.match(/(Trident\/|MSIE\/)/) !== null; 3369 } 3370 }, 3371 getTimestamp: function () { 3372 return (new Date()).getTime(); 3373 }, 3374 /** 3375 * Reduces errors array to single string. 3376 * @param {array} errors 3377 * @return {string} 3378 */ 3379 errorsToString: function (errors) { 3380 if (!this.isArray(errors)) 3381 { 3382 return ""; 3383 } 3384 else 3385 { 3386 return errors.reduce(function (result, currentValue) { 3387 if (result != "") 3388 { 3389 result += "; "; 3390 } 3391 return result + currentValue.code + ": " + currentValue.message; 3392 }, ""); 3393 } 3394 }, 3395 isString: function (item) { 3396 return item === '' ? true : (item ? (typeof (item) == "string" || item instanceof String) : false); 3397 }, 3398 isArray: function (item) { 3399 return item && Object.prototype.toString.call(item) == "[object Array]"; 3400 }, 3401 isFunction: function (item) { 3402 return item === null ? false : (typeof (item) == "function" || item instanceof Function); 3403 }, 3404 isDomNode: function (item) { 3405 return item && typeof (item) == "object" && "nodeType" in item; 3406 }, 3407 isDate: function (item) { 3408 return item && Object.prototype.toString.call(item) == "[object Date]"; 3409 }, 3410 isPlainObject: function (item) { 3411 if (!item || typeof (item) !== "object" || item.nodeType) 3412 { 3413 return false; 3414 } 3415 3416 const hasProp = Object.prototype.hasOwnProperty; 3417 try 3418 { 3419 if (item.constructor && !hasProp.call(item, "constructor") && !hasProp.call(item.constructor.prototype, "isPrototypeOf")) 3420 { 3421 return false; 3422 } 3423 } catch (e) 3424 { 3425 return false; 3426 } 3427 3428 let key; 3429 for (key in item) 3430 { 3431 } 3432 return typeof (key) === "undefined" || hasProp.call(item, key); 3433 }, 3434 isNotEmptyString: function (item) { 3435 return this.isString(item) ? item.length > 0 : false; 3436 }, 3437 isJsonRpcRequest: function (item) { 3438 return ( 3439 typeof (item) === "object" 3440 && item 3441 && "jsonrpc" in item 3442 && Utils.isNotEmptyString(item.jsonrpc) 3443 && "method" in item 3444 && Utils.isNotEmptyString(item.method) 3445 ); 3446 }, 3447 isJsonRpcResponse: function (item) { 3448 return ( 3449 typeof (item) === "object" 3450 && item 3451 && "jsonrpc" in item 3452 && Utils.isNotEmptyString(item.jsonrpc) 3453 && "id" in item 3454 && ( 3455 "result" in item 3456 || "error" in item 3457 ) 3458 ); 3459 3460 }, 3461 buildQueryString: function (params) { 3462 let result = ''; 3463 for (let key in params) 3464 { 3465 if (!params.hasOwnProperty(key)) 3466 { 3467 continue; 3468 } 3469 const value = params[key]; 3470 if (Utils.isArray(value)) 3471 { 3472 value.forEach((valueElement, index) => { 3473 result += encodeURIComponent(key + "[" + index + "]") + "=" + encodeURIComponent(valueElement) + "&"; 3474 }); 3475 } 3476 else 3477 { 3478 result += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&"; 3479 } 3480 } 3481 3482 if (result.length > 0) 3483 { 3484 result = result.substr(0, result.length - 1); 3485 } 3486 3487 return result; 3488 }, 3489 objectValues: function values(obj) { 3490 let result = []; 3491 for (let key in obj) 3492 { 3493 if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key)) 3494 { 3495 result.push(obj[key]); 3496 } 3497 } 3498 return result; 3499 }, 3500 clone: function (obj, bCopyObj) { 3501 let _obj, i, l; 3502 if (bCopyObj !== false) 3503 { 3504 bCopyObj = true; 3505 } 3506 3507 if (obj === null) 3508 { 3509 return null; 3510 } 3511 3512 if (this.isDomNode(obj)) 3513 { 3514 _obj = obj.cloneNode(bCopyObj); 3515 } 3516 else if (typeof obj == 'object') 3517 { 3518 if (this.isArray(obj)) 3519 { 3520 _obj = []; 3521 for (i = 0, l = obj.length; i < l; i++) 3522 { 3523 if (typeof obj[i] == "object" && bCopyObj) 3524 { 3525 _obj[i] = this.clone(obj[i], bCopyObj); 3526 } 3527 else 3528 { 3529 _obj[i] = obj[i]; 3530 } 3531 } 3532 } 3533 else 3534 { 3535 _obj = {}; 3536 if (obj.constructor) 3537 { 3538 if (this.isDate(obj)) 3539 { 3540 _obj = new Date(obj); 3541 } 3542 else 3543 { 3544 _obj = new obj.constructor(); 3545 } 3546 } 3547 3548 for (i in obj) 3549 { 3550 if (!obj.hasOwnProperty(i)) 3551 { 3552 continue; 3553 } 3554 if (typeof obj[i] == "object" && bCopyObj) 3555 { 3556 _obj[i] = this.clone(obj[i], bCopyObj); 3557 } 3558 else 3559 { 3560 _obj[i] = obj[i]; 3561 } 3562 } 3563 } 3564 3565 } 3566 else 3567 {
3568 _obj = obj; 3569 } 3570 3571 return _obj; 3572 }, 3573 3574 getDateForLog: function () { 3575 const d = new Date(); 3576 3577 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'); 3578 }, 3579 3580 lpad: function (str, length, chr) { 3581 str = str.toString(); 3582 chr = chr || ' '; 3583 3584 if (str.length > length) 3585 { 3586 return str; 3587 } 3588 3589 let result = ''; 3590 for (let i = 0; i < length - str.length; i++) 3591 { 3592 result += chr; 3593 } 3594 3595 return result + str; 3596 } 3597 } 3598 3599 if ( 3600 typeof BX.namespace !== 'undefined' 3601 && typeof BX.PULL === 'undefined' 3602 ) 3603 { 3604 BX.PULL = new PullClient(); 3605 } 3606 3607 BX.PullClient = PullClient; 3608 BX.PullClient.PullStatus = PullStatus; 3609 BX.PullClient.SubscriptionType = SubscriptionType; 3610 BX.PullClient.CloseReasons = CloseReasons; 3611 BX.PullClient.StorageManager = StorageManager; 3612})();
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.