• Alex Packard 5
  • NEWBIE
  • 80 Points
  • Member since 2015

  • Chatter
    Feed
  • 0
    Best Answers
  • 10
    Likes Received
  • 1
    Likes Given
  • 7
    Questions
  • 16
    Replies
Spring 21 CSP Problem

In Spring 21 it appears that attempting to load an iframe of a page from a managed package, it gets blocked by Content Security Policy.

Say you have a package whose namespace is "MyNS", which includes a visualforce page named "Inner_Page". Now include that page via iframe in another visualforce page, like so:
 
<apex:page >

    Iframe Test

    <iframe src="MyNS__Inner_Page"/>

</apex:page>

You will see the "Iframe Test" text loads successfully, but the iframe does not and there will be the following error will appear in the browser console:

https://my-org-domain--myns.cs41.visual.force.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".

This is going to break multiple pages in our application when it is released to production.

Has anyone else seen this?  Will it be fixed before Spring 21 is officially released?

 
In Spring 21 it appears that the Node.getRootNode() function is returning undefined instead of the proper behavior, which is to return the ShadowRoot.  (see https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode)
 
Here is a simple component that reproduces the issue:
 
Component
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
    <div id="rooty" aura:id="rooty"/>
</aura:component>


 
Renderer
({
    afterRender : function(component, helper) {
        console.log('root node:');
        console.log(component.find('rooty').getElement().getRootNode());
    }
})


 
Interestingly, it does seem behave correctly inside the Lightning App Builder preview window.  It only returns undefined when you load the actual lightning app/page.  
 
This is causing some big problems for us.  Chart.js relies on this method to behave as defined in the spec and returning undefined breaks it.
 
Has anyone else seen this?  Will this be fixed before Spring 21 is officially released?
https://developer.salesforce.com/forums/ForumsMain?id=9062I000000XnDIQA0

Posted this a while ago, still no solutions.

Help!
If you run into a bug in the Salesforce platform, Salesforce tells you to post here for support.  If you post a bug here, you receive no support.  Usually you don't receive so much as a reply.

Any other frustrated devlopers out there?

Salesforce, how about some accountability when your platform does not work as advertised?
Create the following visualforce page and name it "TestListPage":
 
<apex:page id="TestListPage" standardController="Lead" recordSetVar="records" >

<apex:includeScript value="/support/console/47.0/integration.js"/>

<script>

function openTab(){
sforce.console.openPrimaryTab(null, '/apex/TestListPage', true, 'PrimaryTab', function(result){
console.log(result);
});
}

</script>

<button onClick="openTab()">New Tab</button>

</apex:page>

Go to Setup -> Object Manager -> Lead -> Buttons Links and Actions and override the List action with the page you just created.

In lightning experience, go to the Sales Console and select Leads in the navigation menu.

Your new page should appear instead of the normal lead list. Click the "New Tab" button on the page.

A new console tab should open with the same page in it. In the javascript console, you'll notice a success message.

In this list on the left, click the "New Tab" button again. This time, the new tab will not open and you will see the following error message: "Error: Invalid request."


It appears to me that the salesforce console api breaks completely in the list side of the page once a console tab loads integration.js


A member of the Salesforce Technical Support team assured me that if I posted this here that Salesforce's Developer Experts would review my post and create a Known Issue.  I look forward to your responses!
QuickAction.DescribeQuickActions() will return different results when called from a managed package context and unmanaged code.

For a given entity type, for example Contacts, you must call QuickAction.DescribeAvailableQuickActions('Contact') to get a list of available contact quick action names. Take those quick action names and pass them into QuickAction.DescribeQuickActions(names).
This describe call will only correctly return the quick actions if called from unmanaged code (for example, when called from anonymous apex). However, if this code is called as part of a managed package, the expected quick actions are not returned.

We've noticed that passing in a namespaced quick action names into the method will still work in a managed package.  We suspect that Salesforce is incorrectly trying to add the package's namespace to the quick action names being passed into the call, but this makes it impossible to access non-namespaced quick actions.

Has anyone ever run into this before?  Any ideas on how to get around it?

P.S. I also made an idea for Salesforce to correct this behavior, please vote for it: https://partners.salesforce.com/ideaView?id=0873A0000003b7NQAQ
If you do a SOQL query for a single UserRecordAccess record, you get the correct values. But if you query for multiple UserRecordAccess records in a single query, the values returned are often incorrect (for instance the HasAllAccess value may be true when it should be false).

This problem makes it impossible for us to reliably communicate object permissions to our users.

This is nearly identical to the following issues that was marked as fixed back in the Winter 15 release: https://success.salesforce.com/issues_view?id=a1p30000000T4iiAAC

Below is a test class I wrote that reproduces the issue.  In this test class I use the Macro sobject, but the problem applies to other objects as well.  I first noticed it on a custom object.
@isTest
public without sharing class UserRecordAccessTest {

	private static testMethod void test(){

		//Create the users that will own the records
		final User[] users = createStandardUsers(2);
		final User u1 = users[0];
		final User u2 = users[1];

		//Create the records, each owned by a different user
		final SObject o1 = createRecord(u1.Id);
		final SObject o2 = createRecord(u2.Id);
		insert new SObject[]{o1, o2};

		system.runAs(u1){

			//Query for each UserRecordAccess entry individually
			final UserRecordAccess access1 = queryForSingleUserRecordAccess(u1.Id, o1.Id);
			final UserRecordAccess access2 = queryForSingleUserRecordAccess(u1.Id, o2.Id);

			//Query for both UserRecordAccess entries at the same time
			final UserRecordAccess[] accessList = queryForUserRecordAccesses(u1.Id, new Set<Id>{o1.Id, o2.Id});

			//The UserRecordAccess entries should have the same values, regardless of how they were queried
			compareUserRecordAccesses(access1, accessList[0]);//This fails because the HasAllAccess value is false for the entry queried individually, but is true for the entry queried in a list
			compareUserRecordAccesses(access2, accessList[1]);

		}


	}

	private static UserRecordAccess queryForSingleUserRecordAccess(final Id userId, final Id recordId){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId = :recordId
		];
	}

	private static UserRecordAccess[] queryForUserRecordAccesses(final Id userId, final Set<Id> recordIds){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId IN :recordIds
			ORDER BY RecordId
		];
	}

	private static void compareUserRecordAccesses(final UserRecordAccess expected, final UserRecordAccess actual){

		system.assertEquals(expected.RecordId, 			actual.RecordId);
		system.assertEquals(expected.HasReadAccess, 	actual.HasReadAccess);
		system.assertEquals(expected.HasEditAccess, 	actual.HasEditAccess);
		system.assertEquals(expected.HasDeleteAccess, 	actual.HasDeleteAccess);
		system.assertEquals(expected.HasTransferAccess,	actual.HasTransferAccess);
		system.assertEquals(expected.HasAllAccess, 		actual.HasAllAccess);

	}

	private static User[] createStandardUsers(final Integer count){

		final Profile standardUser = [SELECT Id FROM Profile WHERE Name = 'Standard User'];

		final User[] users = new User[]{};
		for(Integer i = 0; i < count; i++){
			users.add(new User(
				UserName = 'test' + i + '@userrecordaccesstest.com',
				LastName = 'Smith',
				Email = 'test@user.com',
				Alias = 'test',
				CommunityNickname = 'test' + i,
				TimeZoneSidKey = 'America/Indiana/Indianapolis',
				LocaleSidKey = 'en_US',
				EmailEncodingKey = 'UTF-8',
				LanguageLocaleKey = 'en_US',
				ProfileId = standardUser.Id
			));
		}

		insert users;

		return users;
	}

	private static SObject createRecord(final Id ownerId){
		return new Macro(Name = 'Macro' + ownerId, OwnerId = ownerId);
	}

}

 
If you do a SOQL query for a single UserRecordAccess record, you get the correct values. But if you query for multiple UserRecordAccess records in a single query, the values returned are often incorrect (for instance the HasAllAccess value may be true when it should be false).

This problem makes it impossible for us to reliably communicate object permissions to our users.

This is nearly identical to the following issues that was marked as fixed back in the Winter 15 release: https://success.salesforce.com/issues_view?id=a1p30000000T4iiAAC

Below is a test class I wrote that reproduces the issue.  In this test class I use the Macro sobject, but the problem applies to other objects as well.  I first noticed it on a custom object.
@isTest
public without sharing class UserRecordAccessTest {

	private static testMethod void test(){

		//Create the users that will own the records
		final User[] users = createStandardUsers(2);
		final User u1 = users[0];
		final User u2 = users[1];

		//Create the records, each owned by a different user
		final SObject o1 = createRecord(u1.Id);
		final SObject o2 = createRecord(u2.Id);
		insert new SObject[]{o1, o2};

		system.runAs(u1){

			//Query for each UserRecordAccess entry individually
			final UserRecordAccess access1 = queryForSingleUserRecordAccess(u1.Id, o1.Id);
			final UserRecordAccess access2 = queryForSingleUserRecordAccess(u1.Id, o2.Id);

			//Query for both UserRecordAccess entries at the same time
			final UserRecordAccess[] accessList = queryForUserRecordAccesses(u1.Id, new Set<Id>{o1.Id, o2.Id});

			//The UserRecordAccess entries should have the same values, regardless of how they were queried
			compareUserRecordAccesses(access1, accessList[0]);//This fails because the HasAllAccess value is false for the entry queried individually, but is true for the entry queried in a list
			compareUserRecordAccesses(access2, accessList[1]);

		}


	}

	private static UserRecordAccess queryForSingleUserRecordAccess(final Id userId, final Id recordId){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId = :recordId
		];
	}

	private static UserRecordAccess[] queryForUserRecordAccesses(final Id userId, final Set<Id> recordIds){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId IN :recordIds
			ORDER BY RecordId
		];
	}

	private static void compareUserRecordAccesses(final UserRecordAccess expected, final UserRecordAccess actual){

		system.assertEquals(expected.RecordId, 			actual.RecordId);
		system.assertEquals(expected.HasReadAccess, 	actual.HasReadAccess);
		system.assertEquals(expected.HasEditAccess, 	actual.HasEditAccess);
		system.assertEquals(expected.HasDeleteAccess, 	actual.HasDeleteAccess);
		system.assertEquals(expected.HasTransferAccess,	actual.HasTransferAccess);
		system.assertEquals(expected.HasAllAccess, 		actual.HasAllAccess);

	}

	private static User[] createStandardUsers(final Integer count){

		final Profile standardUser = [SELECT Id FROM Profile WHERE Name = 'Standard User'];

		final User[] users = new User[]{};
		for(Integer i = 0; i < count; i++){
			users.add(new User(
				UserName = 'test' + i + '@userrecordaccesstest.com',
				LastName = 'Smith',
				Email = 'test@user.com',
				Alias = 'test',
				CommunityNickname = 'test' + i,
				TimeZoneSidKey = 'America/Indiana/Indianapolis',
				LocaleSidKey = 'en_US',
				EmailEncodingKey = 'UTF-8',
				LanguageLocaleKey = 'en_US',
				ProfileId = standardUser.Id
			));
		}

		insert users;

		return users;
	}

	private static SObject createRecord(final Id ownerId){
		return new Macro(Name = 'Macro' + ownerId, OwnerId = ownerId);
	}

}

 
Spring 21 CSP Problem

In Spring 21 it appears that attempting to load an iframe of a page from a managed package, it gets blocked by Content Security Policy.

Say you have a package whose namespace is "MyNS", which includes a visualforce page named "Inner_Page". Now include that page via iframe in another visualforce page, like so:
 
<apex:page >

    Iframe Test

    <iframe src="MyNS__Inner_Page"/>

</apex:page>

You will see the "Iframe Test" text loads successfully, but the iframe does not and there will be the following error will appear in the browser console:

https://my-org-domain--myns.cs41.visual.force.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".

This is going to break multiple pages in our application when it is released to production.

Has anyone else seen this?  Will it be fixed before Spring 21 is officially released?

 
In Spring 21 it appears that the Node.getRootNode() function is returning undefined instead of the proper behavior, which is to return the ShadowRoot.  (see https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode)
 
Here is a simple component that reproduces the issue:
 
Component
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
    <div id="rooty" aura:id="rooty"/>
</aura:component>


 
Renderer
({
    afterRender : function(component, helper) {
        console.log('root node:');
        console.log(component.find('rooty').getElement().getRootNode());
    }
})


 
Interestingly, it does seem behave correctly inside the Lightning App Builder preview window.  It only returns undefined when you load the actual lightning app/page.  
 
This is causing some big problems for us.  Chart.js relies on this method to behave as defined in the spec and returning undefined breaks it.
 
Has anyone else seen this?  Will this be fixed before Spring 21 is officially released?
If you run into a bug in the Salesforce platform, Salesforce tells you to post here for support.  If you post a bug here, you receive no support.  Usually you don't receive so much as a reply.

Any other frustrated devlopers out there?

Salesforce, how about some accountability when your platform does not work as advertised?
Create the following visualforce page and name it "TestListPage":
 
<apex:page id="TestListPage" standardController="Lead" recordSetVar="records" >

<apex:includeScript value="/support/console/47.0/integration.js"/>

<script>

function openTab(){
sforce.console.openPrimaryTab(null, '/apex/TestListPage', true, 'PrimaryTab', function(result){
console.log(result);
});
}

</script>

<button onClick="openTab()">New Tab</button>

</apex:page>

Go to Setup -> Object Manager -> Lead -> Buttons Links and Actions and override the List action with the page you just created.

In lightning experience, go to the Sales Console and select Leads in the navigation menu.

Your new page should appear instead of the normal lead list. Click the "New Tab" button on the page.

A new console tab should open with the same page in it. In the javascript console, you'll notice a success message.

In this list on the left, click the "New Tab" button again. This time, the new tab will not open and you will see the following error message: "Error: Invalid request."


It appears to me that the salesforce console api breaks completely in the list side of the page once a console tab loads integration.js


A member of the Salesforce Technical Support team assured me that if I posted this here that Salesforce's Developer Experts would review my post and create a Known Issue.  I look forward to your responses!
QuickAction.DescribeQuickActions() will return different results when called from a managed package context and unmanaged code.

For a given entity type, for example Contacts, you must call QuickAction.DescribeAvailableQuickActions('Contact') to get a list of available contact quick action names. Take those quick action names and pass them into QuickAction.DescribeQuickActions(names).
This describe call will only correctly return the quick actions if called from unmanaged code (for example, when called from anonymous apex). However, if this code is called as part of a managed package, the expected quick actions are not returned.

We've noticed that passing in a namespaced quick action names into the method will still work in a managed package.  We suspect that Salesforce is incorrectly trying to add the package's namespace to the quick action names being passed into the call, but this makes it impossible to access non-namespaced quick actions.

Has anyone ever run into this before?  Any ideas on how to get around it?

P.S. I also made an idea for Salesforce to correct this behavior, please vote for it: https://partners.salesforce.com/ideaView?id=0873A0000003b7NQAQ
If you do a SOQL query for a single UserRecordAccess record, you get the correct values. But if you query for multiple UserRecordAccess records in a single query, the values returned are often incorrect (for instance the HasAllAccess value may be true when it should be false).

This problem makes it impossible for us to reliably communicate object permissions to our users.

This is nearly identical to the following issues that was marked as fixed back in the Winter 15 release: https://success.salesforce.com/issues_view?id=a1p30000000T4iiAAC

Below is a test class I wrote that reproduces the issue.  In this test class I use the Macro sobject, but the problem applies to other objects as well.  I first noticed it on a custom object.
@isTest
public without sharing class UserRecordAccessTest {

	private static testMethod void test(){

		//Create the users that will own the records
		final User[] users = createStandardUsers(2);
		final User u1 = users[0];
		final User u2 = users[1];

		//Create the records, each owned by a different user
		final SObject o1 = createRecord(u1.Id);
		final SObject o2 = createRecord(u2.Id);
		insert new SObject[]{o1, o2};

		system.runAs(u1){

			//Query for each UserRecordAccess entry individually
			final UserRecordAccess access1 = queryForSingleUserRecordAccess(u1.Id, o1.Id);
			final UserRecordAccess access2 = queryForSingleUserRecordAccess(u1.Id, o2.Id);

			//Query for both UserRecordAccess entries at the same time
			final UserRecordAccess[] accessList = queryForUserRecordAccesses(u1.Id, new Set<Id>{o1.Id, o2.Id});

			//The UserRecordAccess entries should have the same values, regardless of how they were queried
			compareUserRecordAccesses(access1, accessList[0]);//This fails because the HasAllAccess value is false for the entry queried individually, but is true for the entry queried in a list
			compareUserRecordAccesses(access2, accessList[1]);

		}


	}

	private static UserRecordAccess queryForSingleUserRecordAccess(final Id userId, final Id recordId){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId = :recordId
		];
	}

	private static UserRecordAccess[] queryForUserRecordAccesses(final Id userId, final Set<Id> recordIds){
		return [
			SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess, HasTransferAccess, HasAllAccess
			FROM UserRecordAccess
			WHERE UserId = :userId AND RecordId IN :recordIds
			ORDER BY RecordId
		];
	}

	private static void compareUserRecordAccesses(final UserRecordAccess expected, final UserRecordAccess actual){

		system.assertEquals(expected.RecordId, 			actual.RecordId);
		system.assertEquals(expected.HasReadAccess, 	actual.HasReadAccess);
		system.assertEquals(expected.HasEditAccess, 	actual.HasEditAccess);
		system.assertEquals(expected.HasDeleteAccess, 	actual.HasDeleteAccess);
		system.assertEquals(expected.HasTransferAccess,	actual.HasTransferAccess);
		system.assertEquals(expected.HasAllAccess, 		actual.HasAllAccess);

	}

	private static User[] createStandardUsers(final Integer count){

		final Profile standardUser = [SELECT Id FROM Profile WHERE Name = 'Standard User'];

		final User[] users = new User[]{};
		for(Integer i = 0; i < count; i++){
			users.add(new User(
				UserName = 'test' + i + '@userrecordaccesstest.com',
				LastName = 'Smith',
				Email = 'test@user.com',
				Alias = 'test',
				CommunityNickname = 'test' + i,
				TimeZoneSidKey = 'America/Indiana/Indianapolis',
				LocaleSidKey = 'en_US',
				EmailEncodingKey = 'UTF-8',
				LanguageLocaleKey = 'en_US',
				ProfileId = standardUser.Id
			));
		}

		insert users;

		return users;
	}

	private static SObject createRecord(final Id ownerId){
		return new Macro(Name = 'Macro' + ownerId, OwnerId = ownerId);
	}

}

 

The application I'm working on involves a large number of pages (many of which update dynamically via AJAX actions) mapped to a small set of controllers. Currently, most combinations of page & state are not bookmark-able. The platform seems to take control of the location in the address bar when you're forwarding to another page of the same controller - query parameters are dropped, and the displayed page name often lags behind what's actually active. Creating a bookmark at such a time will result in an error when you try to load the bookmark - either for required query parameters missing, or the requested page being wrong for the active object's current state. Then, there's the whole issue of bookmarking state that was the result of an AJAX action...

 

One typical solution for this is to use Javascript to load the necessary state into the anchor portion of the address bar's location - that is, the part following the leading '#' char (this does not cause the browser to re-load the page, which is why it's useful). The anchor is saved as part of the bookmark, and then when it's loaded, the application logic can use it to re-instantiate the correct state.

 

That brings us to Visualforce. To fully implement this technique, we need a way to read the anchor string from the controller code so that we can instantiate the correct state, forward to the correct subpage, etc. The PageReference's "getAnchor()" method looked promising, but when I tried it out in a test page "/apex/EchoAnchorTest?foo=bar#foobar" - the call "ApexPages.currentPage().getAnchor()" and "System.currentPageReference().getAnchor()" always returned null. I'm really REALLY hoping for a way to access the "#" part of the current URL in Apex. Is there something I'm missing, or has anyone had luck finding an effective workaround?

 

Mike