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