3

I'm trying to build a simple public-facing web form on an LWR Site. One key requirement is for users to be able to upload a file, but I'm facing some heavy obstacles:

I've done a lot of searching on this topic already, and I've seen some recommendations to use the Connect (formerly known as Chatter) API, but I have yet to find any sample code for this approach. The documentation doesn't make it obvious whether you can create a new file at all with this API, and I'm dubious about being able to use it from a Guest User context, since Guest users can't use Chatter.

What can I do?


Update July 2023

I started a thread on the LWR Trailhead Group, and a bunch of other Salesforce customers chimed in saying the lack of a usable file upload component was a big feature gap for them as well. As a result, safe harbor, it looks like they're aiming to deliver a new file upload base component in Spring '24.


Update April 2024

Based on new comments in the Trailhead Group thread, now it's looking like the feature is anticipated for Winter '25.


Update September 2024

File Upload LWC for LWR Sites can be found in the Winter '25 release notes.

2
  • Quick plug, if you're tripping across this page -- please vote this idea up: ideas.salesforce.com/s/idea/a0B8W00000NW57cUAD/… Commented Mar 28, 2023 at 18:39
  • 1
    I had some luck using a custom ApexRest resource. I just wrapped it in an old school Site and then was able to create files as guest users using the js fetch api. I don't know what the file size limits are, but it was at least well more than the 3ish MB you get from an aura enabled method.
    – stackasaur
    Commented Jul 10, 2023 at 19:54

2 Answers 2

4

The answer you're looking for may violate the ToS and/or MSA of salesforce.com, so proceed at your own risk. You can lose all your data, have it stolen, etc. No warranty or guarantee of fitness. Imagine the worst possible scenario if anything goes wrong, and double it. That's the risk we're putting out here.


That said, the solution is rather mundane, though it involves a lot of setup. It's arbitrarily difficult to actually set up something like this on purpose. If you're going to risk your data, you have to work for it.


First, create a sessionId page:

<apex:page
  showHeader="false"
  contentType="text/plain"
  applyBodyTag="false"
  applyHtmlTag="false"
  >{!$Api.Session_ID}</apex:page>

Pay close attention to spacing, a single space can get you an invalid session Id.

Create a Connected App for you to authenticate against. Give it the refresh and offline scopes, as well as API and Id scopes. You may need additional scopes as well. This is mostly theoretical, and not something I've personally done.

Create a profile with minimum access. No field or object security, etc. It should have "Api Only Access" and "Access API" permissions, and nothing else, to the maximum extent possible. The only exceptions are, of course, they'll need the permission to upload whatever kind of file you want to use.

Create an Auth. Provider for Salesforce. Then, a Remote Site setting to allow access to the Salesforce base URL. Also, you need a CORS entry to allow your remote site to access Salesforce. And a Connected App to authenticate against with offline/refresh scopes and api access, with OAuth capability.

Create a user with a Salesforce license that can use the profile you created above. I believe you can get Integration user licenses these days (or soon) that will be helpful in lowering the cost of this setup. After that, a Named Credential will be authenticated with that user, such that you now have API access from anyone that visits the page.

From here, you just need the Salesforce Base URL and the sessionId:

@AuraEnabled(cacheable=true) public static Id sessionId() {
  HttpRequest req = new HttpRequest();
  req.setEndpoint('callout:proxyUser/apex/sessionId');
  req.setMethod('GET');
  return new http().send(req).getBody().toString();
}
@AuraEnabled(cacheable=true) public static String baseUrl() {
  return Url.getSalesforceBaseUrl().toExternalForm();
}

Make sure that you add the class that has these methods to the profile you created earlier. They need this to get data back.

Now, for the easy part. You've got a session Id that you can use, so we just:

@wire(getSessionId) sessionId;
@wire(getBaseUrl) baseUrl;
async uploadFile(file) {
  // We're going to create form data
  const form = new FormData();
  // The PathOnClient attribute must be set.
  form.append(
    'entity_content',
    new Blob([JSON.stringify({ PathOnClient: file.name })], {
      type: 'application/json',
      name: '',
    }),
    ''
  );
  // Attach the File (or Blob) to the VersionData field.
  form.append('VersionData', file);
  // Wait for the upload to finish.
  let result = await fetch(
    `${this.baseUrl}/services/data/v57.0/sobjects/ContentVersion`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.sessionId}`,
      },
      body: form,
    }
  );
  let contentVersion = await result.json();
  // Now, contentVersion.Id is the Id, if successful.
}

You could also probably substitute a normal license for a Chatter license, and use the Chatter API endpoint for uploading files, but I haven't worked with the Chatter API extensively; I found that a straight file upload is the most straight forward.

I may have missed a step or two, and there might be some minor tweaks to some of the other steps, but this probably shouldn't take more than an hour to experiment with to see if it works.

Note that, as a later step, if you need to create the ContentDocumentLink, you can probably just use the normal createRecord method from lightning/uiRecordApi, no shenanigans needed there.

1
  • Another juicy rabbit out of @sfdcfox's hat! Salesforce as its own Connected App. Bravo! I'm very cautious about this approach, as you are. Extreme care would be needed to make sure that session ID could only ever be used to perform the most limited actions, since it would be public info. I will chew on it. In any case -- thank you. Commented Mar 28, 2023 at 13:07
0

One (not-so-great) answer I just discovered: you can iframe a public Aura community page into an LWR site. The Aura page in turn can have an LWC with lightning-file-upload exposed to guest users.

First follow the steps under "Enable Guest Users to Upload Files" in the lightning-file-upload documentation. Create a LWC with a lightning-file-upload component within it. Create a new Aura community as needed, and add your component to a new page in that community. The page should have no navigation header and nothing on it except your component. In your LWR site, iframe the relative URL of the Aura community page.

The iframed page doesn't inherit the LWR site's styling, and the resulting modal when you upload a file is confined to the iframe:

enter image description here

When viewed in Experience Builder, the iframe doesn't render correctly at all.

Caveat: I have not yet tested if it is possible to communicate back and forth with the component in the iframe using Window.postMessage(). This is probably important, and I wouldn't take it for granted.

Not my favorite look, but it might pass as a workaround. I hope I don't have to vote this answer "best".

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .