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