@pendo/agent 2.340.0 → 2.341.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4046,8 +4046,8 @@ let SERVER = '';
4046
4046
  let ASSET_HOST = '';
4047
4047
  let ASSET_PATH = '';
4048
4048
  let DESIGNER_SERVER = '';
4049
- let VERSION = '2.340.0_';
4050
- let PACKAGE_VERSION = '2.340.0';
4049
+ let VERSION = '2.341.0_';
4050
+ let PACKAGE_VERSION = '2.341.0';
4051
4051
  let LOADER = 'xhr';
4052
4052
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4053
4053
  /**
@@ -14305,6 +14305,330 @@ var XHRGuideLoader = {
14305
14305
  }
14306
14306
  };
14307
14307
 
14308
+ function base32Encode(uint8array) {
14309
+ var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
14310
+
14311
+ var length = uint8array.length;
14312
+
14313
+ var bits = 0;
14314
+ var value = 0;
14315
+ var output = '';
14316
+
14317
+ for (var i = 0; i < length; i++) {
14318
+ value = (value << 8) | uint8array[i];
14319
+ bits += 8;
14320
+
14321
+ while (bits >= 5) {
14322
+ output += alphabet[(value >>> (bits - 5)) & 31];
14323
+ bits -= 5;
14324
+ }
14325
+ }
14326
+
14327
+ if (bits > 0) {
14328
+ output += alphabet[(value << (5 - bits)) & 31];
14329
+ }
14330
+
14331
+ return output;
14332
+ }
14333
+
14334
+ // A CNAME contentHost is commonly configured without a scheme, which would leave the rewritten
14335
+ // url relative to the current page rather than absolute. Leading-slash values are deliberate and
14336
+ // kept as-is: '//host' is protocol-relative, '/path' is same-origin (see fixContentHostUrl).
14337
+ function normalizeContentHost(contentHost) {
14338
+ if (/:\/\//.test(contentHost) || /^\//.test(contentHost))
14339
+ return contentHost;
14340
+ return 'https://' + contentHost;
14341
+ }
14342
+ function stripScheme(host) {
14343
+ return host.replace(/^https?:\/\//, '');
14344
+ }
14345
+ // Self hosted installs aim assetHost at their own host, or drop it entirely - the npm package
14346
+ // deletes it for the assets option - so it cannot locate our CDN. SERVER survives either way, and
14347
+ // app, cdn and static are sibling labels in every environment's domain template. Builds that point
14348
+ // SERVER somewhere other than an app host (--env=local) are left with assetHost.
14349
+ function getPendoCdnHost() {
14350
+ var appLabel = /(^|\.)app\./;
14351
+ var serverHost = stripScheme(SERVER);
14352
+ if (!appLabel.test(serverHost))
14353
+ return stripScheme(ASSET_HOST);
14354
+ return serverHost.replace(appLabel, '$1cdn.');
14355
+ }
14356
+ // NOTE: this previously was *only* checking the snippet provided config for contentHost
14357
+ function replaceWithContentHost(str) {
14358
+ var contentHost = ConfigReader.getLocalConfig('contentHost');
14359
+ if (!contentHost || !str)
14360
+ return str;
14361
+ contentHost = normalizeContentHost(contentHost);
14362
+ var cdnHost = getPendoCdnHost();
14363
+ var assetHost = stripScheme(ASSET_HOST);
14364
+ // Content proxy hosts are the environment's domain with static in the variable label, which is
14365
+ // not always the leading one - us1 is content-<subId>.us1.static.pendo.io
14366
+ var staticHost = cdnHost.replace(/(^|\.)cdn\./, '$1static.');
14367
+ // An unknown host has to be dropped rather than interpolated: an empty one leaves a pattern
14368
+ // that matches the // of every absolute url
14369
+ var hostPatterns = [
14370
+ SUBSCRIPTION_BUCKET && `${escapeRegExp(SUBSCRIPTION_BUCKET)}-\\d+\\.storage\\.googleapis\\.com`,
14371
+ GLOBAL_BUCKET && `${escapeRegExp(GLOBAL_BUCKET)}\\.storage\\.googleapis\\.com`,
14372
+ cdnHost && `content-\\d+\\.${escapeRegExp(staticHost)}`,
14373
+ cdnHost && escapeRegExp(cdnHost),
14374
+ assetHost && assetHost !== cdnHost && escapeRegExp(assetHost)
14375
+ ];
14376
+ return _.reduce(hostPatterns, function (rewritten, hostPattern) {
14377
+ if (!hostPattern)
14378
+ return rewritten;
14379
+ return rewritten.replace(new RegExp(`(https:)?\\/\\/${hostPattern}`, 'g'), contentHost);
14380
+ }, str);
14381
+ }
14382
+ function replaceObjectWithContentHost(obj) {
14383
+ var contentHost = ConfigReader.getLocalConfig('contentHost');
14384
+ if (!contentHost)
14385
+ return obj;
14386
+ return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
14387
+ }
14388
+ // APP-37205 we're only checking the remote contentHost config setting
14389
+ function getContentHostRegex() {
14390
+ var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
14391
+ if (ch)
14392
+ return ch + '*';
14393
+ }
14394
+ /**
14395
+ * @access private
14396
+ * @param {Function} isValidGuideResponse
14397
+ * @param {object} guideResponse
14398
+ */
14399
+ function isValidGuideResponse(guideResponse) {
14400
+ // Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
14401
+ // prop, we should also return false.
14402
+ if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
14403
+ guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
14404
+ return false;
14405
+ }
14406
+ return true;
14407
+ }
14408
+ var ContentLoader = (function (pendo) {
14409
+ var cache = {};
14410
+ // For APP-37205, we're verifing that the result is what it is. I know.
14411
+ var replaceWithContentHostAndVerify = _.compose(function (url) {
14412
+ var test = getContentHostRegex();
14413
+ if (test) {
14414
+ var v = new RegExp(test);
14415
+ if (!v.test(url)) {
14416
+ log.debug('Failed to verify URL: ' + url + ' vs ' + test);
14417
+ }
14418
+ }
14419
+ return url;
14420
+ }, replaceWithContentHost);
14421
+ /**
14422
+ * @access private
14423
+ * @typedef {object} ContentRequest
14424
+ * @param {string} id uniquely identifies the container
14425
+ * @param {string} contentUrl
14426
+ * @param {string} contentUrlCss
14427
+ * @param {string} contentUrlJs
14428
+ */
14429
+ /**
14430
+ * @access private
14431
+ * @typedef {object} ContentEntry
14432
+ * @param {string} content The raw content as a string
14433
+ * @param {Function} template The template function compiled from the raw content
14434
+ * @param {Function} script The javascript to execute
14435
+ */
14436
+ /**
14437
+ * Loads the content for the given container
14438
+ *
14439
+ * @access private
14440
+ * @param {ContentRequest} contentContainer
14441
+ * @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
14442
+ */
14443
+ function load(contentContainer) {
14444
+ try {
14445
+ var cacheEntry = cache[contentContainer.id];
14446
+ if (cacheEntry && cacheEntry.language !== contentContainer.language) {
14447
+ delete cache[contentContainer.id];
14448
+ }
14449
+ if (!cache[contentContainer.id]) {
14450
+ var promises = [];
14451
+ var entry = {
14452
+ deferred: {},
14453
+ language: contentContainer.language
14454
+ };
14455
+ var fetchedDomContent = false;
14456
+ cache[contentContainer.id] = entry;
14457
+ if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
14458
+ entry.deferred.domJson = q.defer();
14459
+ ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
14460
+ entry.domJson = replaceObjectWithContentHost(result.data);
14461
+ var guideResponse = result.data;
14462
+ if (isValidGuideResponse(guideResponse)) {
14463
+ entry.deferred.domJson.resolve();
14464
+ }
14465
+ else {
14466
+ // Log that the guide was invalid
14467
+ // handle promise
14468
+ log.critical(new Error('Response from the guide was not in proper JSON format'));
14469
+ entry.deferred.domJson.reject();
14470
+ }
14471
+ }).catch(function (e) {
14472
+ log.critical(e);
14473
+ Events.resourceFetchFail.trigger(contentContainer, e);
14474
+ entry.deferred.domJson.reject('Failed to load content');
14475
+ });
14476
+ promises.push(entry.deferred.domJson.promise);
14477
+ fetchedDomContent = true;
14478
+ }
14479
+ else if (contentContainer.domJsonpUrl) {
14480
+ entry.deferred.domJson = q.defer();
14481
+ var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
14482
+ dom.removeNode(jsonpNode);
14483
+ if (err) {
14484
+ Events.resourceFetchFail.trigger(contentContainer, err);
14485
+ return;
14486
+ }
14487
+ }, true);
14488
+ promises.push(entry.deferred.domJson.promise);
14489
+ fetchedDomContent = true;
14490
+ }
14491
+ if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
14492
+ // Content, template, and script for 2.1+ agents
14493
+ entry.deferred.content = q.defer();
14494
+ var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
14495
+ dom.removeNode(scriptNode);
14496
+ if (err) {
14497
+ Events.resourceFetchFail.trigger(contentContainer, err);
14498
+ return;
14499
+ }
14500
+ }, true);
14501
+ promises.push(entry.deferred.content.promise);
14502
+ // Style
14503
+ if (contentContainer.contentUrlCss) {
14504
+ entry.deferred.css = q.defer();
14505
+ loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
14506
+ entry.deferred.css.resolve();
14507
+ });
14508
+ promises.push(entry.deferred.css.promise);
14509
+ }
14510
+ }
14511
+ else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
14512
+ // Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
14513
+ entry.deferred.content = q.defer();
14514
+ var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
14515
+ dom.removeNode(contentNode);
14516
+ if (err) {
14517
+ Events.resourceFetchFail.trigger(contentContainer, err);
14518
+ return;
14519
+ }
14520
+ }, true);
14521
+ promises.push(entry.deferred.content.promise);
14522
+ }
14523
+ else if (!fetchedDomContent) {
14524
+ return q.reject('fetchedDomContent is still false');
14525
+ }
14526
+ entry.deferred.promise = q.all(promises).then(function () {
14527
+ return _.omit(entry, 'deferred');
14528
+ });
14529
+ }
14530
+ return cache[contentContainer.id].deferred.promise;
14531
+ }
14532
+ catch (e) {
14533
+ Events.resourceFetchFail.trigger(contentContainer, e);
14534
+ throw e;
14535
+ }
14536
+ }
14537
+ // Clears the content cache. Used for testing.
14538
+ function reset() {
14539
+ cache = {};
14540
+ }
14541
+ /**
14542
+ * JSONP-style callback for loading guide content
14543
+ * from script resources. Step content stored in
14544
+ * GCS is wrapped with pendo.guideContent(...)
14545
+ *
14546
+ * @access private
14547
+ * @param {string} guideId
14548
+ * @param {string} stepId
14549
+ * @param {string} content
14550
+ * @param {Function} templateFn
14551
+ * @param {Function} scriptFn
14552
+ */
14553
+ function guideContent(guideId, stepId, content, templateFn, scriptFn) {
14554
+ if (_.isString(content)) {
14555
+ content = preprocessGuideContent(content, guideId, stepId);
14556
+ if (!_.isFunction(templateFn)) {
14557
+ if (shouldCreateTemplate(content)) {
14558
+ templateFn = _.template(content);
14559
+ }
14560
+ else {
14561
+ templateFn = _.constant(content);
14562
+ }
14563
+ }
14564
+ var entry = cache[guideId + stepId];
14565
+ if (entry && entry.deferred.content) {
14566
+ entry.content = content;
14567
+ entry.template = templateFn;
14568
+ entry.script = scriptFn;
14569
+ entry.deferred.content.resolve();
14570
+ }
14571
+ }
14572
+ }
14573
+ // preprocess content to remove simple guide and step IDs templating
14574
+ function removeStandardTemplating(content, guideId, stepId) {
14575
+ const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
14576
+ var guide = pendo.findGuideById('<%= guide.id %>');
14577
+ var step = guide && guide.findStepById('<%= step.id %>');
14578
+ <% } %>`;
14579
+ const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
14580
+ var step = guide && guide.findStepById('${stepId}');`;
14581
+ return content.replace(stdTemplating, sansTemplating);
14582
+ }
14583
+ // execute all content preprocessors and return end result
14584
+ const GuideContentProcessorList = [removeStandardTemplating];
14585
+ function preprocessGuideContent(content, guideId, stepId) {
14586
+ return _.reduce(GuideContentProcessorList, (acc, procFn) => {
14587
+ return procFn(acc, guideId, stepId);
14588
+ }, content);
14589
+ }
14590
+ function shouldCreateTemplate(content) {
14591
+ return /<%/.test(content) && /%>/.test(content);
14592
+ }
14593
+ function receiveDomStructureJson(guideId, stepId, domJson) {
14594
+ var entry = cache[guideId + stepId];
14595
+ if (entry && entry.deferred.domJson) {
14596
+ entry.domJson = replaceObjectWithContentHost(domJson);
14597
+ entry.deferred.domJson.resolve();
14598
+ }
14599
+ }
14600
+ function lightningRedirect(url) {
14601
+ if (isSfdcLightning()) {
14602
+ // https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
14603
+ var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
14604
+ var guideId = path[0];
14605
+ /* global $A */
14606
+ var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
14607
+ if (_.isString(resourcePath)) {
14608
+ return resourcePath + '/' + path.join('/');
14609
+ }
14610
+ }
14611
+ return url;
14612
+ }
14613
+ function addExtensionToUrl(url, extension) {
14614
+ if (!_.isString(url))
14615
+ return url;
14616
+ var i = _.indexOf(url, '?');
14617
+ if (i < 0) {
14618
+ return url + extension;
14619
+ }
14620
+ else {
14621
+ return url.substring(0, i) + extension + url.substring(i);
14622
+ }
14623
+ }
14624
+ return {
14625
+ load,
14626
+ reset,
14627
+ guideContent,
14628
+ receiveDomStructureJson
14629
+ };
14630
+ })();
14631
+
14308
14632
  function getHashFromContentUrl(url) {
14309
14633
  var contentUrl = parseUrl(url);
14310
14634
  var sha1Length = 27;
@@ -14391,10 +14715,12 @@ var ContentVerifier = (function () {
14391
14715
  if (!_.isString(url))
14392
14716
  return q.reject('unable to parse "' + url + '"');
14393
14717
  var stepHash = computeStepHash(contentContainer, urlProp);
14394
- return ajax.get(url).then(function (response) {
14718
+ // Verify against the host the loader fetches from; the hash still comes from the original url.
14719
+ var fetchUrl = replaceWithContentHost(url);
14720
+ return ajax.get(fetchUrl).then(function (response) {
14395
14721
  if (computeHashFromFile(response.data) === stepHash)
14396
14722
  return;
14397
- return q.reject('Unable to verify content at "' + url + '"');
14723
+ return q.reject('Unable to verify content at "' + fetchUrl + '"');
14398
14724
  });
14399
14725
  }
14400
14726
  function verifyContentHashUnlessSri(contentContainer, urlProp) {
@@ -14669,297 +14995,6 @@ function GuideStepFactory(step, guide) {
14669
14995
  GuideFactory.behaviors = [];
14670
14996
  GuideStepFactory.behaviors = [];
14671
14997
 
14672
- function base32Encode(uint8array) {
14673
- var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
14674
-
14675
- var length = uint8array.length;
14676
-
14677
- var bits = 0;
14678
- var value = 0;
14679
- var output = '';
14680
-
14681
- for (var i = 0; i < length; i++) {
14682
- value = (value << 8) | uint8array[i];
14683
- bits += 8;
14684
-
14685
- while (bits >= 5) {
14686
- output += alphabet[(value >>> (bits - 5)) & 31];
14687
- bits -= 5;
14688
- }
14689
- }
14690
-
14691
- if (bits > 0) {
14692
- output += alphabet[(value << (5 - bits)) & 31];
14693
- }
14694
-
14695
- return output;
14696
- }
14697
-
14698
- // NOTE: this previously was *only* checking the snippet provided config for contentHost
14699
- function replaceWithContentHost(str) {
14700
- var contentHost = ConfigReader.getLocalConfig('contentHost');
14701
- if (!contentHost || !str)
14702
- return str;
14703
- var subBucketPattern = new RegExp(`(https:)?\\/\\/${SUBSCRIPTION_BUCKET}-\\d+\\.storage\\.googleapis\\.com`, 'g');
14704
- var globalBucketPattern = new RegExp(`(https:)?\\/\\/${GLOBAL_BUCKET}\\.storage\\.googleapis\\.com`, 'g');
14705
- // Content proxy hosts look like content-<subId>.static.<envDomain>, where envDomain is ASSET_HOST minus its cdn. prefix
14706
- var envDomain = ASSET_HOST.replace(/^cdn\./, '');
14707
- var contentProxyPattern = new RegExp(`(https:)?\\/\\/content-\\d+\\.static\\.${escapeRegExp(envDomain)}`, 'g');
14708
- return str.replace(subBucketPattern, contentHost)
14709
- .replace(globalBucketPattern, contentHost)
14710
- .replace(contentProxyPattern, contentHost)
14711
- .replace(new RegExp(`(https:)?\\/\\/${escapeRegExp(ASSET_HOST)}`, 'g'), contentHost);
14712
- }
14713
- function replaceObjectWithContentHost(obj) {
14714
- var contentHost = ConfigReader.getLocalConfig('contentHost');
14715
- if (!contentHost)
14716
- return obj;
14717
- return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
14718
- }
14719
- // APP-37205 we're only checking the remote contentHost config setting
14720
- function getContentHostRegex() {
14721
- var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
14722
- if (ch)
14723
- return ch + '*';
14724
- }
14725
- /**
14726
- * @access private
14727
- * @param {Function} isValidGuideResponse
14728
- * @param {object} guideResponse
14729
- */
14730
- function isValidGuideResponse(guideResponse) {
14731
- // Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
14732
- // prop, we should also return false.
14733
- if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
14734
- guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
14735
- return false;
14736
- }
14737
- return true;
14738
- }
14739
- var ContentLoader = (function (pendo) {
14740
- var cache = {};
14741
- // For APP-37205, we're verifing that the result is what it is. I know.
14742
- var replaceWithContentHostAndVerify = _.compose(function (url) {
14743
- var test = getContentHostRegex();
14744
- if (test) {
14745
- var v = new RegExp(test);
14746
- if (!v.test(url)) {
14747
- log.debug('Failed to verify URL: ' + url + ' vs ' + test);
14748
- }
14749
- }
14750
- return url;
14751
- }, replaceWithContentHost);
14752
- /**
14753
- * @access private
14754
- * @typedef {object} ContentRequest
14755
- * @param {string} id uniquely identifies the container
14756
- * @param {string} contentUrl
14757
- * @param {string} contentUrlCss
14758
- * @param {string} contentUrlJs
14759
- */
14760
- /**
14761
- * @access private
14762
- * @typedef {object} ContentEntry
14763
- * @param {string} content The raw content as a string
14764
- * @param {Function} template The template function compiled from the raw content
14765
- * @param {Function} script The javascript to execute
14766
- */
14767
- /**
14768
- * Loads the content for the given container
14769
- *
14770
- * @access private
14771
- * @param {ContentRequest} contentContainer
14772
- * @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
14773
- */
14774
- function load(contentContainer) {
14775
- try {
14776
- var cacheEntry = cache[contentContainer.id];
14777
- if (cacheEntry && cacheEntry.language !== contentContainer.language) {
14778
- delete cache[contentContainer.id];
14779
- }
14780
- if (!cache[contentContainer.id]) {
14781
- var promises = [];
14782
- var entry = {
14783
- deferred: {},
14784
- language: contentContainer.language
14785
- };
14786
- var fetchedDomContent = false;
14787
- cache[contentContainer.id] = entry;
14788
- if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
14789
- entry.deferred.domJson = q.defer();
14790
- ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
14791
- entry.domJson = replaceObjectWithContentHost(result.data);
14792
- var guideResponse = result.data;
14793
- if (isValidGuideResponse(guideResponse)) {
14794
- entry.deferred.domJson.resolve();
14795
- }
14796
- else {
14797
- // Log that the guide was invalid
14798
- // handle promise
14799
- log.critical(new Error('Response from the guide was not in proper JSON format'));
14800
- entry.deferred.domJson.reject();
14801
- }
14802
- }).catch(function (e) {
14803
- log.critical(e);
14804
- Events.resourceFetchFail.trigger(contentContainer, e);
14805
- entry.deferred.domJson.reject('Failed to load content');
14806
- });
14807
- promises.push(entry.deferred.domJson.promise);
14808
- fetchedDomContent = true;
14809
- }
14810
- else if (contentContainer.domJsonpUrl) {
14811
- entry.deferred.domJson = q.defer();
14812
- var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
14813
- dom.removeNode(jsonpNode);
14814
- if (err) {
14815
- Events.resourceFetchFail.trigger(contentContainer, err);
14816
- return;
14817
- }
14818
- }, true);
14819
- promises.push(entry.deferred.domJson.promise);
14820
- fetchedDomContent = true;
14821
- }
14822
- if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
14823
- // Content, template, and script for 2.1+ agents
14824
- entry.deferred.content = q.defer();
14825
- var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
14826
- dom.removeNode(scriptNode);
14827
- if (err) {
14828
- Events.resourceFetchFail.trigger(contentContainer, err);
14829
- return;
14830
- }
14831
- }, true);
14832
- promises.push(entry.deferred.content.promise);
14833
- // Style
14834
- if (contentContainer.contentUrlCss) {
14835
- entry.deferred.css = q.defer();
14836
- loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
14837
- entry.deferred.css.resolve();
14838
- });
14839
- promises.push(entry.deferred.css.promise);
14840
- }
14841
- }
14842
- else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
14843
- // Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
14844
- entry.deferred.content = q.defer();
14845
- var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
14846
- dom.removeNode(contentNode);
14847
- if (err) {
14848
- Events.resourceFetchFail.trigger(contentContainer, err);
14849
- return;
14850
- }
14851
- }, true);
14852
- promises.push(entry.deferred.content.promise);
14853
- }
14854
- else if (!fetchedDomContent) {
14855
- return q.reject('fetchedDomContent is still false');
14856
- }
14857
- entry.deferred.promise = q.all(promises).then(function () {
14858
- return _.omit(entry, 'deferred');
14859
- });
14860
- }
14861
- return cache[contentContainer.id].deferred.promise;
14862
- }
14863
- catch (e) {
14864
- Events.resourceFetchFail.trigger(contentContainer, e);
14865
- throw e;
14866
- }
14867
- }
14868
- // Clears the content cache. Used for testing.
14869
- function reset() {
14870
- cache = {};
14871
- }
14872
- /**
14873
- * JSONP-style callback for loading guide content
14874
- * from script resources. Step content stored in
14875
- * GCS is wrapped with pendo.guideContent(...)
14876
- *
14877
- * @access private
14878
- * @param {string} guideId
14879
- * @param {string} stepId
14880
- * @param {string} content
14881
- * @param {Function} templateFn
14882
- * @param {Function} scriptFn
14883
- */
14884
- function guideContent(guideId, stepId, content, templateFn, scriptFn) {
14885
- if (_.isString(content)) {
14886
- content = preprocessGuideContent(content, guideId, stepId);
14887
- if (!_.isFunction(templateFn)) {
14888
- if (shouldCreateTemplate(content)) {
14889
- templateFn = _.template(content);
14890
- }
14891
- else {
14892
- templateFn = _.constant(content);
14893
- }
14894
- }
14895
- var entry = cache[guideId + stepId];
14896
- if (entry && entry.deferred.content) {
14897
- entry.content = content;
14898
- entry.template = templateFn;
14899
- entry.script = scriptFn;
14900
- entry.deferred.content.resolve();
14901
- }
14902
- }
14903
- }
14904
- // preprocess content to remove simple guide and step IDs templating
14905
- function removeStandardTemplating(content, guideId, stepId) {
14906
- const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
14907
- var guide = pendo.findGuideById('<%= guide.id %>');
14908
- var step = guide && guide.findStepById('<%= step.id %>');
14909
- <% } %>`;
14910
- const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
14911
- var step = guide && guide.findStepById('${stepId}');`;
14912
- return content.replace(stdTemplating, sansTemplating);
14913
- }
14914
- // execute all content preprocessors and return end result
14915
- const GuideContentProcessorList = [removeStandardTemplating];
14916
- function preprocessGuideContent(content, guideId, stepId) {
14917
- return _.reduce(GuideContentProcessorList, (acc, procFn) => {
14918
- return procFn(acc, guideId, stepId);
14919
- }, content);
14920
- }
14921
- function shouldCreateTemplate(content) {
14922
- return /<%/.test(content) && /%>/.test(content);
14923
- }
14924
- function receiveDomStructureJson(guideId, stepId, domJson) {
14925
- var entry = cache[guideId + stepId];
14926
- if (entry && entry.deferred.domJson) {
14927
- entry.domJson = replaceObjectWithContentHost(domJson);
14928
- entry.deferred.domJson.resolve();
14929
- }
14930
- }
14931
- function lightningRedirect(url) {
14932
- if (isSfdcLightning()) {
14933
- // https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
14934
- var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
14935
- var guideId = path[0];
14936
- /* global $A */
14937
- var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
14938
- if (_.isString(resourcePath)) {
14939
- return resourcePath + '/' + path.join('/');
14940
- }
14941
- }
14942
- return url;
14943
- }
14944
- function addExtensionToUrl(url, extension) {
14945
- if (!_.isString(url))
14946
- return url;
14947
- var i = _.indexOf(url, '?');
14948
- if (i < 0) {
14949
- return url + extension;
14950
- }
14951
- else {
14952
- return url.substring(0, i) + extension + url.substring(i);
14953
- }
14954
- }
14955
- return {
14956
- load,
14957
- reset,
14958
- guideContent,
14959
- receiveDomStructureJson
14960
- };
14961
- })();
14962
-
14963
14998
  var AsyncContent = (function () {
14964
14999
  function AsyncContent(guide) {
14965
15000
  var step = this;
@@ -15089,7 +15124,7 @@ var GuideRuntime = (function (agentEvents) {
15089
15124
  }
15090
15125
  function teardown() {
15091
15126
  EventChannel.off(handleEvent);
15092
- _.each(pluginAddGlobalScripts, GuideRuntime.removeGlobalScript);
15127
+ pluginAddGlobalScripts.length = 0;
15093
15128
  }
15094
15129
  function addGlobalScript(obj) {
15095
15130
  obj = autoSubscribe(obj);
@@ -23533,7 +23568,7 @@ function autofocusRuntimeScript(step, guide, pendo) {
23533
23568
  if (pendo._.get(step, 'attributes.isAutoFocus')) {
23534
23569
  BuildingBlockGuides.autofocusStep(step, guideContainer);
23535
23570
  BuildingBlockGuides.trapFocusStep(step, guideContainer);
23536
- step.hiddenElements = BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]);
23571
+ step.hiddenElements = (step.hiddenElements || []).concat(BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]));
23537
23572
  }
23538
23573
  if (isGuideRCType(guide)) {
23539
23574
  BuildingBlockGuides.attachRCFocusHandlers();
@@ -25168,22 +25203,31 @@ function hasLegacyGuides(guides, pendo) {
25168
25203
  }
25169
25204
  const DEFAULT_CSS_ID = '_pendo-default-css_';
25170
25205
  const CUSTOM_CSS_ID = '_pendo-css_';
25206
+ // Several SDK instances can share a page. Scoping the <link> ids by api key stops one app's
25207
+ // teardown, or its "no custom css" path, from stripping the stylesheet another app injected.
25208
+ function getCssElementId(baseId) {
25209
+ return pendo$1.apiKey ? baseId + pendo$1.apiKey : baseId;
25210
+ }
25211
+ // Api keys are arbitrary customer-supplied strings, so '#' + id is not a safe selector.
25212
+ function removeGuideCss(baseId) {
25213
+ dom(document.getElementById(getCssElementId(baseId))).remove();
25214
+ }
25171
25215
  function loadGuideCss() {
25172
25216
  var promises = [];
25173
25217
  if (!shouldLoadGlobalCSS()) {
25174
25218
  return q.resolve();
25175
25219
  }
25176
25220
  if (hasLegacyGuides(getActiveGuides(), pendo$1)) {
25177
- promises.push(loadExternalCss(DEFAULT_CSS_ID, getDefaultCssUrl()));
25221
+ promises.push(loadExternalCss(getCssElementId(DEFAULT_CSS_ID), getDefaultCssUrl()));
25178
25222
  }
25179
25223
  var guideWidget = pendo$1.guideWidget || {};
25180
25224
  var data = guideWidget.data || {};
25181
25225
  var customCssUrl = data.guideCssUrl;
25182
25226
  if (customCssUrl) {
25183
- promises.push(loadExternalCss(CUSTOM_CSS_ID, replaceWithContentHost(customCssUrl)));
25227
+ promises.push(loadExternalCss(getCssElementId(CUSTOM_CSS_ID), replaceWithContentHost(customCssUrl)));
25184
25228
  }
25185
25229
  else {
25186
- dom('#' + CUSTOM_CSS_ID).remove();
25230
+ removeGuideCss(CUSTOM_CSS_ID);
25187
25231
  }
25188
25232
  return promises;
25189
25233
  }
@@ -25400,8 +25444,8 @@ var initGuides = function (observer) {
25400
25444
  _.each(teardownFns, function (teardownFn) {
25401
25445
  teardownFn();
25402
25446
  });
25403
- dom('#' + DEFAULT_CSS_ID).remove();
25404
- dom('#' + CUSTOM_CSS_ID).remove();
25447
+ removeGuideCss(DEFAULT_CSS_ID);
25448
+ removeGuideCss(CUSTOM_CSS_ID);
25405
25449
  GuideRuntime.teardown();
25406
25450
  };
25407
25451
  };
@@ -25871,6 +25915,8 @@ _.extend(TopFrameRelay.prototype, {
25871
25915
  if (!message.data)
25872
25916
  return;
25873
25917
  if (message.data.action === 'pendo::connect') {
25918
+ if (!isSameWindowTree(message, this._window))
25919
+ return;
25874
25920
  this._impl.frameConnect(message);
25875
25921
  }
25876
25922
  if (message.data.action === REMOVE_FRAME_ACTION) {
@@ -25966,6 +26012,38 @@ var ImplCommon = {
25966
26012
  function isPortValid(port) {
25967
26013
  return port && _.all(['addEventListener', 'start'], function (cmd) { return _.isFunction(port[cmd]); });
25968
26014
  }
26015
+ // A connect is only ever posted to window.top by a frame below us, so a sender we
26016
+ // cannot find in our own frame tree is not one of our frames no matter what it
26017
+ // claims. window.open gives an attacker a handle to post through, and neither
26018
+ // X-Frame-Options nor frame-ancestors covers that - they only govern embedding
26019
+ // (APP-168708). frames and its indexed children are readable across origins, so
26020
+ // this holds where an origin check can't: no ancestorOrigins needed, and sandboxed
26021
+ // frames (origin "null") are still found.
26022
+ function isFrameBelowUs(source, window) {
26023
+ if (!source)
26024
+ return false;
26025
+ if (source === window)
26026
+ return true;
26027
+ var pending = [window];
26028
+ while (pending.length) {
26029
+ var children = pending.shift().frames;
26030
+ var count = children ? children.length : 0;
26031
+ for (var i = 0; i < count; i++) {
26032
+ if (children[i] === source)
26033
+ return true;
26034
+ pending.push(children[i]);
26035
+ }
26036
+ }
26037
+ return false;
26038
+ }
26039
+ function isSameWindowTree(message, window) {
26040
+ try {
26041
+ return isFrameBelowUs(message.source, window);
26042
+ }
26043
+ catch (e) {
26044
+ return false;
26045
+ }
26046
+ }
25969
26047
  function MessageChannelImpl(window, singletonMessageHandler) {
25970
26048
  this._window = window;
25971
26049
  this._ports = {};
@@ -43105,6 +43183,124 @@ function substitutionIcon(color, size) {
43105
43183
  13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
43106
43184
  }
43107
43185
 
43186
+ const AUTO_ADVANCE_DELAY_MS = 250;
43187
+ function isAutoAdvanceRequested(step, _) {
43188
+ return _.get(step, 'attributes.autoAdvance') === true;
43189
+ }
43190
+ // Survey-authored DOM carries no question-type classes, so eligibility keys off
43191
+ // answer-control shape: every required poll id must be radio-backed, which also
43192
+ // refuses a future non-radio type by default.
43193
+ function isStepEligibleInDom(step, _, requiredPollIds) {
43194
+ const guideElement = step && step.guideElement;
43195
+ if (!guideElement)
43196
+ return false;
43197
+ if (guideElement.find('textarea, input:text, input:checkbox, select').length)
43198
+ return false;
43199
+ const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
43200
+ if (!pollIds.length)
43201
+ return false;
43202
+ return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
43203
+ guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
43204
+ }
43205
+ function hideSubmitButtons(step, _, submitButtons) {
43206
+ const hiddenButtons = [];
43207
+ _.each(submitButtons, (button) => {
43208
+ const element = step.guideElement.find(`#${button.props.id}`)[0];
43209
+ if (!element)
43210
+ return;
43211
+ // display, not visibility: a reserved-but-invisible box leaves an empty gap
43212
+ // at the foot of the step. Authored styles reach the button inline
43213
+ // (build.js:151), so the original value is kept rather than blanked.
43214
+ hiddenButtons.push({ element, display: element.style.display });
43215
+ element.style.display = 'none';
43216
+ });
43217
+ return function restoreSubmitButtons() {
43218
+ _.each(hiddenButtons, (hiddenButton) => {
43219
+ hiddenButton.element.style.display = hiddenButton.display;
43220
+ });
43221
+ };
43222
+ }
43223
+ // The container's pixel height is measured during render (layout.js) before custom
43224
+ // scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
43225
+ // the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
43226
+ // a second guide on screen is left alone.
43227
+ function reflowGuide(step, _, pendo) {
43228
+ const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
43229
+ try {
43230
+ if (_.isFunction(pendo.flexElement)) {
43231
+ pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
43232
+ }
43233
+ if (_.isFunction(recalculateGuideHeight)) {
43234
+ recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
43235
+ }
43236
+ }
43237
+ catch (error) {
43238
+ pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
43239
+ }
43240
+ }
43241
+ function createInertController() {
43242
+ return { isActive: false, notifyAnswered() { }, cancel() { } };
43243
+ }
43244
+ function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
43245
+ const _ = pendo._;
43246
+ const inert = createInertController();
43247
+ if (!isAutoAdvanceRequested(step, _))
43248
+ return inert;
43249
+ // Only reachable for API-authored guides; choosing between two
43250
+ // submitPollAndGoToStep targets would be a guess.
43251
+ if (_.size(submitButtons) !== 1) {
43252
+ pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
43253
+ return inert;
43254
+ }
43255
+ if (!isStepEligibleInDom(step, _, requiredPollIds)) {
43256
+ pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
43257
+ return inert;
43258
+ }
43259
+ const submitButton = submitButtons[0];
43260
+ // A click binds every action on the node and dispatches one event each
43261
+ // (build.js:205-207); we dispatch one, so a second action would be dropped.
43262
+ if (_.size(submitButton.actions) !== 1) {
43263
+ pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
43264
+ return inert;
43265
+ }
43266
+ const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
43267
+ if (!submitAction)
43268
+ return inert;
43269
+ let hasFired = false;
43270
+ let restoreSubmitButtons = _.noop;
43271
+ function fire() {
43272
+ if (hasFired)
43273
+ return;
43274
+ hasFired = true;
43275
+ try {
43276
+ // Shape-identical to what bindActionToNode emits for a click, so
43277
+ // beforeAdvance consumers intercept an auto-advance the same way.
43278
+ step.eventRouter.eventable.trigger('pendoEvent', {
43279
+ action: submitAction.action,
43280
+ params: submitAction.parameters,
43281
+ step,
43282
+ ignore: !!submitAction.ignore,
43283
+ srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
43284
+ });
43285
+ }
43286
+ catch (error) {
43287
+ restoreSubmitButtons();
43288
+ reflowGuide(step, _, pendo);
43289
+ pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
43290
+ }
43291
+ }
43292
+ const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
43293
+ // Last, so nothing between here and the return can throw and leave the step
43294
+ // with a hidden button and no controller to un-hide it.
43295
+ restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
43296
+ reflowGuide(step, _, pendo);
43297
+ return {
43298
+ isActive: true,
43299
+ notifyAnswered: debouncedFire,
43300
+ cancel: debouncedFire.cancel
43301
+ };
43302
+ }
43303
+
43108
43304
  const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
43109
43305
  const requiredSyntax = '{required/}';
43110
43306
  const RequiredQuestions = {
@@ -43114,6 +43310,7 @@ const RequiredQuestions = {
43114
43310
  let requiredPollIds = [];
43115
43311
  let submitButtons = (_a = guideMarkdownUtil.lookupGuideButtons(step.domJson)) === null || _a === void 0 ? void 0 : _a.filter(button => button.actions.find(action => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep'));
43116
43312
  const requiredQuestions = processRequiredQuestions();
43313
+ let autoAdvance = createInertController();
43117
43314
  if (requiredQuestions) {
43118
43315
  pendo._.forEach(requiredQuestions, (question) => {
43119
43316
  if (question.classList.contains('_pendo-open-text-poll-question')) {
@@ -43129,6 +43326,17 @@ const RequiredQuestions = {
43129
43326
  }
43130
43327
  });
43131
43328
  }
43329
+ // Armed last, after the listeners above: runCustomScripts abandons the rest of
43330
+ // this script on a throw, and the submit button is already disabled by now.
43331
+ if (isAutoAdvanceRequested(step, pendo._)) {
43332
+ this.on('beforeUnmount', () => autoAdvance.cancel());
43333
+ try {
43334
+ autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
43335
+ }
43336
+ catch (error) {
43337
+ pendo.log('[auto-advance] arming failed, leaving the step on manual submit', error && error.message);
43338
+ }
43339
+ }
43132
43340
  function getEligibleQuestions() {
43133
43341
  const pollQuestions = pendo._.toArray(step.guideElement.find(`[class*=-poll-question]:contains(${requiredSyntax})`));
43134
43342
  const surveyMatches = pendo._.toArray(step.guideElement.find(`[data-pendo-poll-id]:contains(${requiredSyntax})`));
@@ -43205,9 +43413,11 @@ const RequiredQuestions = {
43205
43413
  }
43206
43414
  if (allRequiredComplete) {
43207
43415
  enableEligibleButtons(submitButtons);
43416
+ autoAdvance.notifyAnswered();
43208
43417
  }
43209
43418
  else {
43210
43419
  disableEligibleButtons(submitButtons);
43420
+ autoAdvance.cancel();
43211
43421
  }
43212
43422
  }
43213
43423
  function disableEligibleButtons(buttons) {