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