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