Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .forceignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
**/*.ts
**/tsconfig*.json
**/*.tsbuildinfo
**/eslint.config.mjs
**/eslint.config.mjs
**/jsconfig.json

**/.eslintrc.json
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ target/
node_modules/
/.illuminatedCloud/
**/tsconfig*.json
**/*.tsbuildinfo
**/*.tsbuildinfo
package-lock.json
package.json
8 changes: 8 additions & 0 deletions force-app/main/default/classes/SummitEventsLinkHandler.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public with sharing class SummitEventsLinkHandler {
public static void run(Map<Id,Summit_Events__c> newMap) {
new SummitEventsRtfLinkPipeline(newMap).run();
}

// Future consideration: add async logic here

}
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>65.0</apiVersion>
<status>Active</status>
</ApexClass>
283 changes: 283 additions & 0 deletions force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
public without sharing class SummitEventsRtfLinkPipeline {

// Inelegant, but schema introspection can't differentiate RTF from regular text fields
private static final Set<String> RICHTEXT_FIELDS = new Set<String>{
'Event_Full_Text__c', 'Event_Confirmation_Description__c',
'Event_Additional_Questions_Description__c',
'Event_Appointment_Description__c', 'Event_Footer__c',
'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c',
'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c',
'Event_description__c', 'Donation_Description__c',
'Guest_Registration_Description__c'
};

private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix('');

private Map<Id,ContentDistribution> cvIdToCDMap;
private List<SummitEventWrapper> wrappedEventList;
private List<Id> idList;
private String executionStage;

public SummitEventsRtfLinkPipeline(Map<Id,Summit_Events__c> newMap) {
this.cvIdToCDMap = new Map<Id,ContentDistribution>();
this.wrappedEventList = new List<SummitEventWrapper>();
this.idList = new List<Id>(newMap.keySet());
}

public void run() {
this.executionStage = 'run';
try {
filter();
scan();
prepare();
create();
push();
} catch(Exception e) {
System.debug('Pipeline failed at stage ' + this.executionStage + ': ' + e.getMessage());
}
}

/**
* Cheap first-pass filter to select and wrap Summit_Events__c records
* that have RTF fields with links to ContentVersion records.
*/
@TestVisible
private void filter() {

this.executionStage = 'filter';

List<Summit_Events__c> freshEventList = new List<Summit_Events__c>();
List<String> fields = new List<String>();
String query = '';

Summit_Events__c schemaObject = new Summit_Events__c();
Map<String,SObjectField> fieldMap =
schemaObject
.getSObjectType()
.getDescribe()
.fields
.getMap();

for(String field : RICHTEXT_FIELDS) {

Boolean isUpdateable = fieldMap.get(namespace + field).getDescribe().isUpdateable();

if(isUpdateable) {
fields.add(namespace + field);
}
}

if(fields.isEmpty()) {
throw new SummitEventsException('No RTF fields found or user lacks sufficient permissions to update fields');
}

String fieldList = String.join(fields, ', ');
query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList';
freshEventList = Database.query(query);

for(Summit_Events__c freshEvent : freshEventList) {
SummitEventWrapper wrappedEvent = new SummitEventWrapper();
for (String field : fields) {
String fieldVal = (String) freshEvent?.get(field);
if(!String.isBlank(fieldVal) && fieldVal.contains('<img src=') && fieldVal.contains('versionId=068')) {
wrappedEvent.fieldsWithLinks.add(field);
}
}
if(!wrappedEvent.fieldsWithLinks.isEmpty()) {
wrappedEvent.event = freshEvent;
this.wrappedEventList.add(wrappedEvent);
}
}
}

/**
* Scan through wrapped events and find all the ContentVersion Ids.
* Pattern and Matcher would have less cognitive overhead, but they
* come attached to significant time complexity, so we're going to
* roll our own manual string ops. Even without direct access to char
* arrays, this should still be O(n).
*/
@TestVisible
private void scan() {

this.executionStage = 'scan';

for (SummitEventWrapper wrappedEvent : wrappedEventList) {
Map<String, List<SummitFieldItem>> tempMap = new Map<String, List<SummitFieldItem>>();

for (String field : wrappedEvent.fieldsWithLinks) {
String html = (String) wrappedEvent.event.get(field);
List<SummitFieldItem> items = new List<SummitFieldItem>();

Integer pos = 0;
while (true) {
// Find the next <img src="...">
Integer tagStart = html.indexOf('<img', pos);
if (tagStart == -1)
break;

Integer srcStart = html.indexOf('src="', tagStart);
if (srcStart == -1)
break;

srcStart += 5; // past src="
Integer srcEnd = html.indexOf('"', srcStart);
if (srcEnd == -1)
break;

String url = html.substring(srcStart, srcEnd);
pos = srcEnd + 1;

// Look for versionId
Integer idIdx = url.indexOf('versionId=068');
if (idIdx == -1)
continue;

Integer idStart = idIdx + 'versionId='.length();
Integer i = idStart;
String alphaNumerics = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
while (i < url.length() && alphaNumerics.indexOf(url.substring(i, i + 1)) != -1)
i++;

String possibleId = url.substring(idStart, i);
if (possibleId.startsWith('068')) {
SummitFieldItem sfi = new SummitFieldItem();
sfi.cvId = (Id) possibleId;
sfi.url = url;
items.add(sfi);
}
}

if (!items.isEmpty()) {
tempMap.put(field, items);
}
}
wrappedEvent.fieldsToLinkMap = tempMap;
}
}

/**
* Prepare the pipeline for execution; populate some final values
* we'll need for execution.
*/
@TestVisible
private void prepare() {

this.executionStage = 'prepare';

Set<Id> cvIdSet = new Set<Id>();
for(SummitEventWrapper wrappedEvent : wrappedEventList) {
for(String field : wrappedEvent.fieldsToLinkMap.keySet()) {
for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) {
cvIdSet.add(sfi.cvId);
}
}
}

List<ContentDistribution> cdList = [
SELECT Id,
DistributionPublicUrl,
Name,
ContentVersionId
FROM ContentDistribution
WHERE ContentVersionId IN :cvIdSet
];

for(ContentDistribution cd : cdList) {
this.cvIdToCDMap.put(cd.ContentVersionId, cd);
}
}

/**
* Create ContentDistribution records for any ContentVersion records
* that are not already linked to a ContentDistribution record.
*/
@TestVisible
private void create() {

this.executionStage = 'create';

List<ContentDistribution> cdInsertList = new List<ContentDistribution>();
Set<Id> visited = new Set<Id>();

for(SummitEventWrapper wrappedEvent : wrappedEventList) {
for(String field : wrappedEvent.fieldsToLinkMap.keySet()) {
Integer i = 0;
for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) {
if(!cvIdToCDMap.containsKey(sfi.cvId) && !visited.contains(sfi.cvId)) {
ContentDistribution cd = new ContentDistribution();
cd.ContentVersionId = sfi.cvId;
cd.Name = wrappedEvent.event.Name + ' - ' + field + i++;
cdInsertList.add(cd);
visited.add(sfi.cvId);
}
}
}
}

if(!cdInsertList.isEmpty()) {
insert cdInsertList;

List<ContentDistribution> refreshed = [
SELECT Id,
DistributionPublicUrl,
Name,
ContentVersionId
FROM ContentDistribution
WHERE Id IN :cdInsertList
];

for(ContentDistribution cd : refreshed) {
cvIdToCDMap.put(cd.ContentVersionId, cd);
}
}
}

/**
* Replace all URLs in the RTF fields with the corresponding ContentDistribution
* record's DistributionPublicUrl, and update the Summit_Events__c record.
*/
@TestVisible
private void push() {

this.executionStage = 'push';

List<Summit_Events__c> updateList = new List<Summit_Events__c>();

for(SummitEventWrapper wrappedEvent : wrappedEventList) {
Summit_Events__c tempEvent = new Summit_Events__c();
tempEvent.Id = wrappedEvent.event.Id; // trigger records are immutable, we must create new records
for(String field : wrappedEvent.fieldsWithLinks) {
String rtf = (String) wrappedEvent.event.get(field);
for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) {
rtf = rtf.replace(sfi.url, cvIdToCDMap.get(sfi.cvId).DistributionPublicUrl);
}
tempEvent.put(field,rtf);
}
updateList.add(tempEvent);
}

if(!updateList.isEmpty()) {
this.executionStage = 'push.DML';
update updateList;
}
}

// Simple wrapper to aggregate relevant values we'll use
private class SummitEventWrapper {
Summit_Events__c event;
Set<String> fieldsWithLinks;
Map<String,List<SummitFieldItem>> fieldsToLinkMap;

private SummitEventWrapper() {
this.fieldsWithLinks = new Set<String>();
this.fieldsToLinkMap = new Map<String,List<SummitFieldItem>>();
}
}

private class SummitFieldItem {
String url;
Id cvId;
private SummitFieldItem() {}
}
}
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>65.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Opt_Into_Link_Automation__c</fullName>
<defaultValue>false</defaultValue>
<description>Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs.</description>
<externalId>false</externalId>
<inlineHelpText>Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs.</inlineHelpText>
<label>Opt into link automation</label>
<trackTrending>false</trackTrending>
<type>Checkbox</type>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Turn_off_Summit_Events_Trigger__c</fullName>
<defaultValue>false</defaultValue>
<description>Turns off Summit Event App's Summit Events trigger.</description>
<externalId>false</externalId>
<inlineHelpText>Turns off Summit Event App's Summit Events trigger.</inlineHelpText>
<label>Turn off Summit Events Trigger</label>
<trackTrending>false</trackTrending>
<type>Checkbox</type>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
trigger SummitEventsSummitEventTrigger on Summit_Events__c (before insert, before update, after insert, after update) {
Summit_Events_Settings__c SummitEventsSettings = Summit_Events_Settings__c.getOrgDefaults();
if (!SummitEventsSettings.Turn_off_Summit_Events_Trigger__c) {
if (SummitEventsSettings.Opt_Into_Link_Automation__c) {
if (Trigger.isInsert && Trigger.isAfter) {
SummitEventsLinkHandler.run(Trigger.newMap);
}
if (Trigger.isUpdate && Trigger.isAfter) {
SummitEventsLinkHandler.run(Trigger.newMap);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexTrigger>
Loading