Skip to content

Commit

Permalink
Merge pull request #177 from SFDO-Community/feature/175
Browse files Browse the repository at this point in the history
Feature/175
  • Loading branch information
iandrosov authored Feb 24, 2025
2 parents f38de5d + f78f88e commit 1f6c8fc
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 18 deletions.
67 changes: 56 additions & 11 deletions force-app/main/default/classes/GGW_ApplicationCtrl.cls
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ public with sharing class GGW_ApplicationCtrl {
// Delete logo file from grant
@AuraEnabled
public static String deleteLogo(String recordId){
//Check Field Level Security
Boolean canAccessGrantApp = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'DistributionPublicUrl__c','Logo_Download_Url__c','Include_Logo__c' }, 'Upsert');

GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = recordId;
app.DistributionPublicUrl__c = null; // Logo public URL
app.Logo_Download_Url__c = null; // Logo display URL
app.Include_Logo__c = false;
// Check object CRUD
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() && canAccessGrantApp){
update app;
}
// Find and delete content file and related records
Expand All @@ -45,10 +49,15 @@ public with sharing class GGW_ApplicationCtrl {
// Include or exclude logo image into grant application
@AuraEnabled
public static String includeLogo(String recordId, Boolean state){
//Check Field Level Security
Boolean canAccessGrantApp2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'Include_Logo__c' }, 'Upsert');


GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = recordId;
app.Include_Logo__c = state;
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() && canAccessGrantApp2){
update app;
}
return 'Application logo updated';
Expand Down Expand Up @@ -85,13 +94,17 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static void saveSelectedSectionText(String itemid, String blockid){

Boolean canAccessSelectedItem = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'Selected_Block__c','Text_Block__c' }, 'Upsert');

GGW_Content_Block__c cBlock = GGW_ContentBlockSelector.queryContentBlockById(blockid);
// Construct selected Item to update
GGW_Selected_Item__c item = new GGW_Selected_Item__c();
item.Id = itemid;
item.Selected_Block__c = blockid;
item.Text_Block__c = cBlock.Description__c; // Copy rich text from block to item for edits
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() && canAccessSelectedItem){
update item;
}
}
Expand All @@ -103,25 +116,32 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static GGW_SectionWrapper createNewSection(String name){
Boolean canAccessSection= GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Section__c', new List<String>{'Name', 'Recommended__c', 'Suggested__c', 'Language__c', 'Sort_Order__c' }, 'Upsert');

GGW_Section__c maxOrder = GGW_SectionSelector.findMaxOrderSection();
GGW_Section__c s = new GGW_Section__c();
s.Name = name;
s.Recommended__c = true;
s.Suggested__c = true;
s.Language__c = GGW_Util.getGrantLanguage();
s.Sort_Order__c = getSectionSortOrder(maxOrder);
if(Schema.sObjectType.GGW_Section__c.isCreateable()){
if(Schema.sObjectType.GGW_Section__c.isCreateable() && canAccessSection){
insert s;
}
return new GGW_SectionWrapper(s);
}
// Edit rich text inside item method called from Section component when edit rich text
@AuraEnabled
public static void updateSelectedItemText(String itemid, String richtext){
//Check FLS
Boolean canAccessSelectedItem2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'Text_Block__c'}, 'Upsert');

GGW_Selected_Item__c item = new GGW_Selected_Item__c();
item.Id = itemid;
item.Text_Block__c = richtext; // Update rich text from block to item on edit button click
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() && canAccessSelectedItem2){
update item;
}
}
Expand All @@ -138,6 +158,10 @@ public with sharing class GGW_ApplicationCtrl {
@AuraEnabled
public static void reorderSections(List<String> sectionList, String appId){
List<GGW_Selected_Item__c> updateOrderList = new List<GGW_Selected_Item__c>();
//Check FLS
Boolean canAccessSelectedItem3 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'GGW_Section__c','Grant_Application__c','Sort_Order__c'}, 'Upsert');

// Clean up items for reorder, delete items that are NOT on this list
cleanSelectedSections(sectionList, appId);
Integer cnt = 1;
Expand All @@ -160,7 +184,7 @@ public with sharing class GGW_ApplicationCtrl {
cnt++;
}
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() ||
Schema.sObjectType.GGW_Selected_Item__c.isCreateable()){
Schema.sObjectType.GGW_Selected_Item__c.isCreateable() && canAccessSelectedItem3){
upsert updateOrderList; // Some records here exist some may be new added sections
}
}
Expand Down Expand Up @@ -206,11 +230,15 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static String addTextBlockToLibrary(String sectionid, String richtext, String name){
//Check FLS
Boolean canAccessContentBlock = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Content_Block__c', new List<String>{'name','Section__c','Description__c'}, 'Upsert');

GGW_Content_Block__c cb = new GGW_Content_Block__c();
cb.name = getValidBlockName(name); // strange error Layout Field:Name must not be Readonly
cb.Section__c = sectionid;
cb.Description__c = richtext;
if(Schema.sObjectType.GGW_Content_Block__c.isCreateable()){
if(Schema.sObjectType.GGW_Content_Block__c.isCreateable() && canAccessContentBlock){
insert cb;
}
return cb.Id+'';
Expand Down Expand Up @@ -354,25 +382,34 @@ public with sharing class GGW_ApplicationCtrl {
return null;
}
private static void updateGrantAppLogoURL(String grantId, ContentDistribution cdURL){
//Check FLS
Boolean canAccessGrantApp3 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'DistributionPublicUrl__c','Logo_Download_Url__c'}, 'Upsert');

// Update Grant with new logo
if(cdURL.ContentDownloadUrl != null){
GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = grantId;
app.DistributionPublicUrl__c = cdURL.DistributionPublicUrl; // Logo public URL
app.Logo_Download_Url__c = cdURL.ContentDownloadUrl; // Logo display URL
// Check object CRUD
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() ){
update app;
}
}
}
private static ContentDistribution insertContentDistribution(ContentVersion file){
//Check FLS

Boolean canAccessCD = GGW_PermissionValidator.getInstance()
.hasStandFLSAccessForFields('ContentDistribution', new List<String>{'Name','ContentVersionId', 'PreferencesAllowViewInBrowser'}, 'insert');

ContentDistribution cdr = new ContentDistribution(
Name = file.Title,
ContentVersionId = file.Id,
PreferencesAllowViewInBrowser = true );
// Check object CRUD
if(Schema.sObjectType.ContentDistribution.isCreateable()){
if(Schema.sObjectType.ContentDistribution.isCreateable() ){
insert cdr;
}
return cdr;
Expand Down Expand Up @@ -471,18 +508,26 @@ public with sharing class GGW_ApplicationCtrl {
}

private static GGW_Grant_Application__c insertGrantRecord(String name){
//Check FLS
Boolean canAccessGrantApp4 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'Name','Status__c','Language__c'}, 'Upsert');

GGW_Grant_Application__c gapp = new GGW_Grant_Application__c();
if (GGW_Util.isValidString(name)){
gapp.Name = name;
gapp.Status__c = 'In Progress';
gapp.Language__c = GGW_Util.getGrantLanguage();
if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable() && canAccessGrantApp4){
insert gapp;
}
}
return gapp;
}
private static void insertSelectedItemsForGrant(Id appId, List<String> sections){
//Check FLS
Boolean canAccessSI2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'GGW_Section__c','Grant_Application__c','Sort_Order__c'}, 'Upsert');

// Add selected sections itterate over selected section IDs param
List<GGW_Selected_Item__c> selectedItems = new List<GGW_Selected_Item__c>();
Integer itemSortOrder = 1;
Expand All @@ -494,7 +539,7 @@ public with sharing class GGW_ApplicationCtrl {
selectedItems.add(item);
itemSortOrder++; // increment sort order set as default
}
if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable() && canAccessSI2){
insert selectedItems;
}
}
Expand Down
5 changes: 4 additions & 1 deletion force-app/main/default/classes/GGW_ApplicationCtrlTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ public class GGW_ApplicationCtrlTest {
}
}
GGW_Grant_Application__c app = GGW_ApplicationCtrl.newGrant('Content Distribution Test', sections);

System.debug('Grant App: '+ app.Id);

ContentVersion cvo = new Contentversion();
cvo.Title = 'Test Content file';
cvo.PathOnClient = 'test';
Expand All @@ -186,7 +189,7 @@ public class GGW_ApplicationCtrlTest {
String downloadURL = GGW_ApplicationCtrl.createContentDistribution(app.Id, cvo.Id);
Test.stopTest();

System.assertNotEquals(null, downloadURL, 'Contenet distribution is invalid');
System.assertNotEquals(null, downloadURL, 'Content distribution is invalid');
GGW_GrantApplicationWrapper grant = GGW_ApplicationCtrl.getApplication(app.Id);
System.assertEquals(downloadURL, grant.logodisplayurl, 'Logo download URL is not valid');
}
Expand Down
164 changes: 164 additions & 0 deletions force-app/main/default/classes/GGW_PermissionValidator.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*
* GGW_PermissionValidator class used to support checking object and field level access while using the Grant Content Kit.
*
*/
public with sharing class GGW_PermissionValidator {
@TestVisible
private static GGW_PermissionValidator instance;

public static GGW_PermissionValidator getInstance() {
if (instance == null) {
instance = new GGW_PermissionValidator();
}
return instance;
}

private static final String NAMESPACE = '%%%NAMESPACED_ORG%%%';

public enum CRUDAccessType {
CREATEABLE,
READABLE,
UPDATEABLE,
DELETEABLE
}

public Boolean hasFLSAccessForFields(
String objectName,
List<String> fields,
String operation
) {
return hasFLSAccessForFields(objectName, fields, operation, true);
}

public Boolean hasFLSAccessForFields(
String objectName,
List<String> fields,
String operation,
Boolean strictMode
) {
try {
String nameSpacedObjectName = NAMESPACE + objectName;
Schema.DescribeSobjectResult[] results = Schema.describeSObjects(

new List<String>{ nameSpacedObjectName }
);
Map<String, Schema.SObjectField> fieldsMap = results[0].fields.getMap();

for (String fieldName : fields) {
//Prepend the Namespace if it exists in the Environment
String nameSpacedFN = NAMESPACE + fieldname;

if (strictMode && !fieldsMap.containsKey(nameSpacedFN)) {
return false;
} else if (!strictMode && !fieldsMap.containsKey(nameSpacedFN)) {
return true;
} else if (
operation == 'insert' &&
!fieldsMap.get(nameSpacedFN).getDescribe().isCreateable()
) {
return false;
} else if (
operation == 'upsert' &&
(!fieldsMap.get(nameSpacedFN).getDescribe().isCreateable() ||
!fieldsMap.get(nameSpacedFN).getDescribe().isUpdateable())
) {
return false;
} else if (
operation == 'read' &&
!hasFieldReadAccess(fieldsMap.get(nameSpacedFN).getDescribe())
) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
//FLS Check for Standard Objects without Namespace

public Boolean hasStandFLSAccessForFields(
String objectName,
List<String> fields,
String operation
) {
return hasStandFLSAccessForFields(objectName, fields, operation, true);
}

public Boolean hasStandFLSAccessForFields(
String objectName,
List<String> fields,
String operation,
Boolean strictMode
) {
try {

Schema.DescribeSobjectResult[] results = Schema.describeSObjects(

new List<String>{ ObjectName }
);
Map<String, Schema.SObjectField> fieldsMap = results[0].fields.getMap();

for (String fieldName : fields) {

if (strictMode && !fieldsMap.containsKey(fieldname)) {
return false;
} else if (!strictMode && !fieldsMap.containsKey(fieldname)) {
return true;
} else if (
operation == 'insert' &&
!fieldsMap.get(fieldname).getDescribe().isCreateable()
) {
return false;
} else if (
operation == 'upsert' &&
(!fieldsMap.get(fieldname).getDescribe().isCreateable() ||
!fieldsMap.get(fieldname).getDescribe().isUpdateable())
) {
return false;
} else if (
operation == 'read' &&
!hasFieldReadAccess(fieldsMap.get(fieldname).getDescribe())
) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}

public Boolean hasFieldReadAccess(DescribeFieldResult field) {
return field.isAccessible();
}

public Boolean hasObjectAccess(SObjectType sObjectType, CRUDAccessType accessType) {
if (sObjectType == null) {
return false;
}

switch on accessType {
when CREATEABLE {
return sObjectType.getDescribe().isCreateable();
}
when READABLE {
return sObjectType.getDescribe().isAccessible();
}
when UPDATEABLE {
return sObjectType.getDescribe().isUpdateable();
}
when DELETEABLE {
return sObjectType.getDescribe().isDeletable();
}
when else {
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading

0 comments on commit 1f6c8fc

Please sign in to comment.