[Aura]ゲストユーザで問合せフォームに添付ファイルをアップロードする方法

# 1. はじめに
この記事に辿り着いた方はご存知と思いますが、Experience Cloud(旧Community Cloud)においてゲストユーザはデフォルトではファイルをアップロードできません。
また、フローだけを使ってコーディング無しで要件を満たすことも2022/3時点では実現出来ません。
詳細理由とフローをベースにしたゲストユーザのファイルアップロード&レコードとの紐付け方法は、[コチラ](https://trailblazer.salesforce.com/issues_view?id=a1p4V000001JiOMQA0)をご確認下さい。
この記事では、Lightning Aura ComponentとApexのみでゲストユーザによるファイルのアップロードと新規作成したレコードとの紐付けを1トランザクションで実行する方法を範囲とします。
レコード作成フォームをフルカスタマイズしたい場合を仮定し、フローを使わない方式でやってみます。

# 2. サイトの公開アクセス
作成したサイトは、不特定多数の人へ公開する機能がExperience Cloudにはあります。
デフォルトでは非公開になっていますが、以下のように設定することで公開されます。
公開状態になるとゲストユーザ用のURLが表示されます。
公開サイトのことを、この記事ではゲストユーザがアクセス可能な状態と表現します。
エクスペリエンスビルダー > 歯車(設定) > 全般タブ > サイトの詳細セクション
<img width="600" alt="公開アクセス" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/aa83c11f-f85c-7473-46e4-23c6d515ed48.png">

# 3. ゲストユーザへのファイルアップロード許可
ゲストユーザには、デフォルトではファイルのアップロード行為が許可されておらず、明示的に許可する設定を行う必要があります。
以下設定項目の吹き出しを確認すると、「lightning:fileUpload」なるコンポーネントを使用するとアップロードが出来るとあります。
エクスペリエンスビルダーで提供されている標準のコンポーネントでは、ライセンスを持ったユーザ向けにのみアップロード機能があります。しかし、ゲストユーザ向けのサンプルコンポーネントにはアップロード機能はありません！
つまり、「lightning:fileUpload」を使って開発をしなければならないということです。
"lightning:"とある様にLightning Aura Componentで開発しないと、ゲストユーザファイルアップロードが出来ません。
設定 > 機能設定 > Salesforce Files > 一般設定
<img width="800" alt="Salesforce Files" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/9de1b625-d891-df37-0821-800959f92b8b.png">

# 4. lightning:fileUploadコンポーネント利用
先ほどヒントがあった「lightning:fileUpload」をGoogleで検索すると、Aura Componentの[サンプルコード](https://developer.salesforce.com/docs/component-library/bundle/lightning:fileUpload)が出てきます。
LWCでは「lightning-file-upload」コンポーネントが同機能を提供するようですが、本記事の対象からは割愛します。
サンプルコードに従いauraを採用します。
読み進めると、Guest_Record_fileupload__cに紐付けたいレコードをセットすると、アップロードと同時に紐付けが行われるとあります。
レコード編集時ならともかく、新規レコード作成時にはIDはまだ発番されていません。
新規レコードに関連付ける為には、予めアップロードしたファイルID情報が必要であると考えることが出来ます。
アップロードしたファイルIDを内部で保持しておき、レコードが作成された後で紐付けを行う方法にします。
以上より公式ドキュメントに反し、ContentVersionオブジェクトにGuest_Record_fileuploadというカスタム項目は作らずに進めます。
``` apex:lightning：fileUpload利用例
<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId">
    <aura:attribute name="filetype" type="List" default="['.png', '.jpg', '.jpeg']" />
    <aura:attribute name="multiple" type="Boolean" default="true" />
    <aura:attribute name="disabled" type="Boolean" default="true" />
    <aura:attribute name="recordId" type="String" />
    <aura:attribute name="encryptedToken" type="String" />
    <lightning:fileUpload label="Attach receipt"
        name="fileUploader"
        multiple="true"
        accept="{!v.filetype}"
        recordId="{!v.recordId}"
        fileFieldName="Guest_Record_fileupload__c"
        fileFieldValue="{!v.encryptedToken}"
        onuploadfinished="{!c.handleUploadFinished}" />
</aura:component>
```

# 5. ファイル関連オブジェクトリレーション確認
紐付け処理を検討するにあたり、ファイルオブジェクトのリレーションを[公式ドキュメント](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_erd_content.htm)から確認します。
必要な部分を、必要なアトリビュートに絞って抜き出すと、以下のER図になります。
ファイルをアップロードすると、最新バージョンを管理するContentDocumentと、ファイルの実体であるContentVersionが作成されます。
そして、ContentDocumentLinkオブジェクトを連関オブジェクトとして、ファイルとレコードを関連付ける仕組みであると読み取れます。
LinkedEntityIdには、ケースだけではなく任意のレコードのIDを指定出来る様になっています。
<img width="600" alt="Content Document Link" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/484cce88-a523-7865-0633-734d2960bae2.png">
ファイルをアップロードしたタイミングで取得できる（筈の）DocumentIdと、ケースを作成した際に得られるCase Idが分かれば紐付けは出来そうです。

__新規作成レコードにファイルを紐付ける順序(想定)__
　①ファイルアップロード時にDocumentIdを取得する
　②ケースを登録し、CaseIdを取得する
　③ケース登録直後に、DocumentIdとCaseIdを紐づけるContentDocumentLinkレコードを作成する


# 6. lightning:fileUploadを使ったアップロード実験
以下javascriptで、アップロードしたファイルのContentDocumentIDを取得できる事を確認してみます。①の実証実験です。
``` javascript
({
    //アップロード後処理
    handleUploadFinished: function (component, event, helper) {
        
        var uploadedFiles = event.getParam("files");
        
        uploadedFiles.forEach(file => {
            console.log("contentVersionId:"+file.contentVersionId);
            console.log("documentId:"+file.documentId); 
            console.log("name:"+ file.name);
        });     
    }
})
```
ファイルをアップロードして、ブラウザのコンソールを確認してみると、、、
<img width="350" alt="Console" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/ec350e74-30d9-c595-accd-63c65ac990c9.png">
あれ、undefined? ..documentId取れてない..?
どうやら、ただではDocumentIdは取れない様です。
ER図に立ち戻って、作戦を練り直します。
ContentVersionIdが分かれば、リレーションを辿ってContentDocumentIdを突き止める事が出来そうです。
では、以下の様に作戦を変更します。

___新規作成レコードにファイルを紐付ける順序(改)___
　①ファイルアップロード時にContentVersionIdを取得する (ContentDocumentIdは、取れない..)
　①’SOQLを使って、ContentVersionIdをWhere句に指定し、ContentDocumentIdを取得する
　②ケースを登録し、CaseIdを取得する
　③DocumentIdとCaseIdを紐づけるContentDocumentLinkを挿入する

# 7. ContentDocumentLinkによるレコードとファイルの関連付け処理

先ほどアップロードしたファイルのContentVersionIdと、適当な新規作成ケースのIdを引数に指定して、紐付けられるか実験してみます。
ContentVersionオブジェクトの中から、ContentVersionIdを指定して対象のレコードを取り出します。①'~③の実証実験です。
以下コードのSOQL部は、取り出したレコードの1属性であるContentDocumentIdを取得しています。
[リファレンス](https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentdocumentlink.htm)によると、「外部ユーザは Visibility を AllUsers にのみ設定できます」とあるので、選択の余地はありません。
ケースIdと、ContentDocumentIdを指定してContentDocumentLinkレコードを作成する流れです。
Apexを実行。ケースレコード画面をリロードすると。。。意図したとおりにファイルが紐付いています。良かった良かった
これで、①~③を連続して処理することでケース作成&アップロード済みファイルの関連付けが出来ることが証明されました。
```java:AttachmentController.cls
public without sharing class AttachmentController {
    

    public static void CreateContentDocumentLink(Case newCase, List<String> documents) {
        
        //ケース登録
        insert newCase;
        //紐づけオブジェクト宣言
        List<ContentDocumentLink> newLinks = new List<ContentDocumentLink>();
        
        //添付ファイル分ループ
        for(String docverId : documents) {
            
            //ContentVersionIdを元に、CondentDocumentIdを取得
            ContentVersion cv = [SELECT Id,ContentDocumentId FROM ContentVersion WHERE Id =:docverId];
            
            ContentDocumentLink newLink = new ContentDocumentLink(
                ContentDocumentId = cv.ContentDocumentId,
                LinkedEntityId = newCase.Id, //発番されたケースID
                Visibility = 'AllUsers',
                ShareType = 'v'                    
            );
            newLinks.add(newLink);
        }
        
        //紐づけオブジェクト挿入
        insert newLinks;           
    }
}
```


# 8. ゲストユーザへApexClassの実行を許可

ケース登録の流れでゲストユーザがApexを実行出来る様に、プロファイルへ直接対象Classの実行権限を与えます。
「ユーザ > プロファイル」からゲスト用プロファイルを探すと、何故か対象プロファイルは見つかりません。
お気づきの方も居るかと思いますが、[2章](#2-サイトの公開アクセス)のエクスペリエンスビルダーの公開アクセス設定の下に、ゲスト用プロファイルへの入口があります。
<img width="500" alt="Guest Profile" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/827ed4c6-2c4a-0ed9-5e43-0002fd34382b.png">


# 9. Aura Componentでケースフォーム画面準備
メールアドレス、氏名、件名、説明の4つの入力ボックスのあるフォームを用意します。
以下画像の様な感じにします。
<img width="550" alt="Form" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2192642/1f2d2c4d-0fd4-07d6-28a9-5ab7cc6900ea.png">
フォーム内に、アップロードコンポーネントを含めているのがポイントです。
Aura Componentの記載は、こんな感じです。
```html:GuestInquiryForm.cmp
<aura:component implements="forceCommunity:availableForAllPageTypes"
                access="global" 
                controller="AttachmentController">
    
    <aura:attribute name="newCase" type="Case" default="{ 'Type': 'Question',
                                                          'Origin':'コミュニティ',
                                                          'Status':'New',                                                        
                                                          'SuppliedEmail' : ''},
                                                          'SuppliedName' : ''},
                                                          'Subject' : ''},
                                                          'Description' : ''}"/>
    <aura:attribute name="documents" type="List" default="[]"/>
    <aura:attribute name="filenames" type="List" default="[]"/>
    <aura:attribute name="accept" type="List" default="['.jpg', '.jpeg', '.png', '.gif', '.bmp','.xls','.xlsx','.zip','.lzh']"/>
    <aura:attribute name="multiple" type="Boolean" default="true"/>
    <aura:attribute name="disabled" type="Boolean" default="false"/>
    <aura:attribute name="isComplete" type="Boolean" default="false"/>

	<article class="slds-card slds-m-horizontal_medium ">
        <div class="slds-grid slds-einstein-header slds-card__header">
            <header class="slds-media slds-media_center slds-has-flexi-truncate">
                <div class="slds-grid slds-grid_vertical-align-center slds-size_3-of-4 slds-medium-size_2-of-3">
                    <div class="slds-media__body">
                        <h2 class="slds-truncate" title="Einstein (10+)">                          
                                <span class="slds-text-heading_small">MKIサポートセンター</span>                            
                        </h2>
                    </div>
                </div>
                <div class="slds-einstein-header__figure slds-size_1-of-4 slds-medium-size_1-of-3"></div>
            </header>
        </div>
        
        <aura:if isTrue="{!not(v.isComplete)}">
            <div class="Create slds-m-horizontal_medium slds-m-top_medium">      
                <lightning:card class="headerLabel" iconName="action:new_case" title="お問い合わせフォーム">  
                    <form class="slds-form--stacked">  
                        <lightning:input aura:id="SuppliedEmail" 
                                             label="メールアドレス" 
                                             name="SuppliedEmail"
                                             value="{!v.newCase.SuppliedEmail}"
                                             required="true"
                                             class="input slds-m-horizontal_large slds-m-vertical_medium slds-m-top_x-large"
                                             maxlength="80"/>
        
                        <lightning:input aura:id="SuppliedName" 
                                             label="氏名" 
                                             name="SuppliedName"
                                             value="{!v.newCase.SuppliedName}"
                                             required="true"
                                             class="input slds-m-horizontal_large slds-m-vertical_medium"
                                             maxlength="80"/>
                     
                        <lightning:input aura:id="Subject" 
                                             label="件名" 
                                             name="Subject"
                                             value="{!v.newCase.Subject}"
                                             required="true"
                                             class="input slds-m-horizontal_large slds-m-vertical_medium"
                                             maxlength="80"/>
        
                         <lightning:textarea aura:id="Description"
                                             name="Description"
                                             label="説明"
                                             value="{!v.newCase.Description}"
                                             class="input slds-m-horizontal_large slds-m-top_medium"
                                             required="true"/>
                        
                         <div class="input slds-m-horizontal_large">
                             <lightning:fileUpload name="fileUploader"
                                             multiple="{!v.multiple}"
                                             accept="{!v.accept}"
                                             disabled="{!v.disabled}"
                                             onuploadfinished="{!c.onUploadFinished }"/>
                        </div>  
                        
                        <aura:iteration items="{!v.filenames}" var="doc">
                            <p class="slds-m-horizontal_large slds-text-title_bold ">{!doc}</p>
                        </aura:iteration>
                        
                        <div class="slds-m-vertical_large slds-align_absolute-center">
                        <lightning:button label="登録"
                                class="communityBtn slds-button custombtn"
             
                                          variant="brand"
                                onclick="{!c.SubmitCase}"/>
                        </div>
                    
                    </form>
                </lightning:card>
            </div>
            
            <aura:set attribute="else">
                  <p class="SuccessPanel">
                    <lightning:card title="" class="SuccessPanel vertical-center">
                        <div class="slds-list_horizontal slds-align_absolute-center slds-m-bottom_large">
                            <lightning:icon iconName="utility:success" alternativeText="Success!" variant="Success"
                                    title="success variant xx-small" size="xx-small" />
                            <h2 class="slds-text-title_bold">　正常に送信されました</h2>
                        </div>     
                        <h2 class = "slds-m-bottom_large">弊社サポートよりご回答申し上げますので、今しばらくお待ち下さい</h2>          
                        <lightning:button label="閉じる" title="close button"  class="slds-m-left_x-small buttonwide"/>
                    </lightning:card>
                </p>
            </aura:set>          
        </aura:if>         

        <footer class="slds-card__footer">
        </footer>
    </article>   
</aura:component>
```

ファイルをアップロードする度に、コントローラーのonUploadFinishedを実行しています。
登録ボタンをクリックすると、コントローラーのSubmitCase経由で、Apexクラス(ケースとファイルの紐付け)を実行する仕掛けです。
ケースとファイルの紐付けまで正常に終了すると、aura ifによりthank youコンポーネントに切り替えます。

# 10. Auraコントローラ処理内容
コントローラには、コンポーネントから呼び出される関数を2つ定義します。
一つ目は、アップロードが完了する度に呼ばれるonUploadFinishedです。
アップロードされたファイルのDocumentVersionIdを、後で紐付ける為にリストに追加していきます。
今現在どのファイルがアップロード済みなのかをユーザに表示する為に、ファイル名もリストに追加します。
これらリストは、コンポーネントアトリビュートとして定義されています。
(一度アップロードしたファイルのキャンセル処理は省略)
```javascript:GuestInquiryFormController.js
onUploadFinished: function (component, event, helper) {

    var uploadedFiles = event.getParam("files");

    //ファイル-レコード紐付け用
    var documentVersionIds = component.get("v.documents");
    uploadedFiles.forEach(element => documentVersionIds.push(element.contentVersionId));
    component.set("v.documents", documentVersionIds);

    //画面表示用ファイル名リスト追加    
    var filenames = component.get("v.filenames"); 
    uploadedFiles.forEach(element => filenames.push("・"+element.name));
    component.set("v.filenames", filenames);
}
```

2つ目は、登録ボタンを押下した際に呼ばれるsubmit処理です。
ケースを登録し、発番されたCase IdとDocumentVersionIdのリストをApex Classに渡します。
紐付けが完了すると、正常メッセージを表示する為に、切替フラグisCompleteにTrueをセットしています。
フラグがTrueになると、aura ifによりコンポーネントが自動的に切り替わります。
```javascript:GuestInquiryFormController.js
SubmitCase : function(component, event, helper) {

    var action = component.get("c.CreateContentDocumentLink");

    action.setParams ({
        documents : component.get("v.documents"),   //アップロードファイルリスト
        newCase : component.get("v.newCase")        //ケースオブジェクト
    });

    action.setCallback(this, function(response) {

        var state = response.getState();

        if (state==="SUCCESS") {
            //aura:ifのフラグ設定、正常終了メッセージを表示
            component.set("v.isComplete", "true");
        }
        else {
            let errors = response.getError();
            let message = 'エラーが発生しました';
            if (errors && Array.isArray(errors) && errors.length > 0) {
                message = errors[0].message;
            }
            console.error(message);
        }
    });
    $A.enqueueAction(action);
}
```


# 11. 最後に
作ったコンポーネントを、公開サイトに埋め込んで動作を確認します。
以下アニメーションの様に、ゲストユーザによりレコードが作成され、同時にアップロードファイルが無事紐付きました。
公開サイトは、不特定多数の人がアクセス出来る為、セキュリティ設計を行う必要があります。
セキュリティの考慮は、[Salesforceのベストプラクティス](https://www.learnexperiencecloud.com/s/article/Guest-User-Record-Access-Development-Best-Practices)を参考にして下さい。
<img width="400" alt="sample movie" src="https://user-images.githubusercontent.com/75467209/157459338-20f70705-961d-4c78-8ccb-8f2f7a4cc331.GIF">

今回のサンプルコードは、リンクからダウンロードして試せます。

https://github.com/okabe-ryota/GuestFileUpload

<font color="MediumSlateBlue">
$\tiny{※この実験サンプルコードによよって生じた損害等に関して、一切の責任を負いかねます}$
</font>

## 参考
- [File Upload Screen component in Flows will not work for guest users](https://trailblazer.salesforce.com/issues_view?id=a1p4V000001JiOMQA0)
- [Lightning Componentを使ったゲストユーザファイルアップロードパターン](https://www.learnexperiencecloud.com/s/article/Guest-User-Record-Access-Development-Best-Practices)
- [file uploadコンポーネントリファレンス](https://developer.salesforce.com/docs/component-library/bundle/lightning:fileUpload/documentation)
- [コンテンツのオブジェクトER図](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_erd_content.htm)