外部フォームブロックをカスタマイズし、入力・確認・完了画面のフォームを作る

nakazawa
nakazawa

外部フォームブロックとは

Concrete CMSには外部フォームというブロックがあります。
通常のフォームブロックでは用意されていない処理を行いたい場合など、色々とカスタマイズを行えます。
カスタマイズに必要な基本ファイルは、コントローラーファイルとビューファイルです。
concreteディレクトリにある、それらのファイルをapplicationディレクトリへ同じフォルダ構成でそれぞれ設置してカスタマイズしていきます。
 
コントローラー
/blocks/external_form/form/controller/xxxxx.php
ビュー
/blocks/external_form/form/xxxxx.php
 

入力・確認・完了と遷移するフォームを作成

それでは、concreteディレクトリのexternal_formにtest_form.phpという名前でコントローラーファイルとビューファイルがあるので、それぞれコピーしてapplicationディレクトリに設置してください。
コントローラーファイルのnamespaceとclass名を忘れずに直しておきましょう。
 
initial.png
 
次に、フォームブロックを設置し、ブロック編集画面を開きます。
追加したフォームが選択肢に表示されるので、それを選んでください。
 
Screen Shot 2021-10-08 at 15.50.48.png
 
この段階では、既存のtest_form.phpをコピーしただけなので、入力画面と完了画面だけの簡易的な表示になっています。
 
入力画面
 
input.png
 
完了画面
 
submit.png
 
 
ここからは、カスタマイズ方法について詳しく説明していきます。
今回は、入力・確認・完了という流れのフォームを作っていきたいと思います。
 
まず、入力画面のフォームを作っていきましょう。
 
ビュー
<form method="post" action="<?= h($view->action('go_to_conf')) ?>">
    <?=Core::make('token')->output('go_to_conf');?>
    <p style="font-size: 20px;font-weight: 700; color: darkgoldenrod;"><?= $message ?></p>
    <div class="form-group">
        <label class="control-label"><?= h('Text Field') ?></label>
        <?= $form->text('foo_bar_text_field') ?>
    </div>
    <div class="form-group">
        <input type="submit" name="submit" value="submit" class="btn btn-default"/>
    </div>
</form>
コントローラー
public function action_go_to_conf($bID = false)
{
    $token = \Core::make("token");

    if ($this->bID == $bID && $token->validate('go_to_conf')) {
        $this->set('message', h('Conf Page'));
        $fooBarTextField = $this->get('foo_bar_text_field');
        $input['foo_bar_text_field'] = $fooBarTextField;
        $this->set('input', $input);
        $this->set('mode', h('conf'));
        return true;
    }
}
入力画面

Screen Shot 2021-10-08 at 17.18.51.png

重要なのはaction属性です。
formタグのaction属性の名前とコントローラーのfunction名を一致させてください。
ここでは、go_to_confというアクション名を設定したので、function名は「action_」 +「 go_to_conf」 となります。
 
次に、確認画面のフォームを作ってきます。
 
ビュー
<!--確認画面-->
<p style="font-size: 20px;font-weight: 700; color: darkgoldenrod;"><?= h($message) ?></p>
<p>Your input is <span style="font-size:20px; font-weight:700;"><?= h($input['foo_bar_text_field']) ?></span>.</p>
<div style="display:flex;">
    <form method="post" action="<?= h($view->action('go_to_comp')) ?>" style="margin-right: 10px;">
        <div class="form-group">
            <input type="submit" name="submit" value="submit" class="btn btn-default"/>
        </div>
    </form>
    <form method="post" action="<?= h($view->action('back_to_edit')) ?>" class="test">
        <div class="form-group">
            <?= $form->hidden('foo_bar_text_field', h($input['foo_bar_text_field'])) ?>
            <div class="form-group">
                <input type="submit" name="submit" value="back" class="btn btn-default"/>
            </div>
        </div>
    </form>
</div>
コントローラー
public function action_go_to_comp($bID = false)
{
    if ($this->bID == $bID) {
        $this->set('message', h('Comp Page'));
        $this->set('response', h('Thanks!'));
        $this->set('mode', h('comp'));
        return true;
    }
}

public function action_back_to_edit($bID = false)
{
    if ($this->bID == $bID) {
        $this->set('mode', h('edit'));
        return true;
    }
}
入力画面

Screen Shot 2021-10-08 at 17.21.47.png

確認画面

Screen Shot 2021-10-08 at 17.21.56.png

 
先ほどと同様に、action属性とコントローラーのfunction名を一致させてください。
確認画面なので戻るボタンがありますが、入力画面に戻った時に入力値が入った状態にしたい場合は、hiddenデータを渡してあげましょう。
 
<form method="post" action="<?= h($view->action('back_to_edit')) ?>" class="test">
    <div class="form-group">
        //ここ↓
        <?= $form->hidden('foo_bar_text_field', h($input['foo_bar_text_field'])) ?>
        <div class="form-group">
            <input type="submit" name="submit" value="back" class="btn btn-default"/>
        </div>
    </div>
</form>
 
あとは、入力画面フォーム、管理画面フォームをそれぞれ表示するための条件分岐をつけていきます。
今回は、$modeという変数に「edit」「conf」「comp」という値をそれぞれ設定し、判定用に使用しました。
view()action_go_to_conf()action_go_to_comp()action_back_to_edit()の処理が実行される時に設定してください。
変数名、値は任意の名前で構いませんが、設定方法は下記の通りです。
このように設定することで、'conf'という値を持った$mode変数をビュー側で使うことができます。
 
$this->set('mode', h('conf'));
 

セキュリティ対策

フォームを作る上で、セキュリティ対策も欠かせません。
Concrete CMSの外部フォームでは、クロスサイトスクリプティング(XSS)クロスサイト・リクエスト・フォージェリ(CSRS)への対応も可能です。
 
両者ともサイバー攻撃の一種ですが、攻撃の内容が少し異ります。
クロスサイトスクリプティングとは、Webアプリの脆弱性を利用し、ユーザーがサイトにアクセスすることで、悪意のあるスクリプトが実行されて個人情報などを盗む攻撃手法です。
一方、クロスサイト・リクエスト・フォージェリは、ログインしているユーザーに悪意のあるリンクを踏ませ、あたかも、そのユーザー自身が操作したかのようにWebアプリに不正なリクエストを送信する攻撃手法です。
 
ではクロスサイトスクリプティングの対策から説明していきます。
Concrete CMSでは、テキストをh()で囲うことでhtmlspecialchars関数の処理(特殊文字を HTML エンティティに変換する)を実行できます。
具体的に確認していきましょう。
 
まず、悪意のあるスクリプトが実行されるという想定で、入力欄にスクリプトを書いて送信してみます。
 
Screen Shot 2021-10-14 at 15.41.47.png
 
送信後、入力画面から受け取った値をh()で囲わずに表示してみます。
 
Screen Shot 2021-10-14 at 15.41.16.png
 
送信すると、スクリプトが実行されたことが確認できました。
 
Screen Shot 2021-10-14 at 15.49.35.png
 
では、h()で囲って再度確認してみます。
 
Screen Shot 2021-10-14 at 15.51.42.png
 
今度は、スクリプトは実行されず、ただの文字列として表示されました。
 
Screen Shot 2021-10-14 at 15.53.59.png
h()を忘れずに使い、クロスサイトスクリプティング対策を行いましょう。
 
次に、クロスサイト・リクエスト・フィージェリへの対策方法を説明していきます。
こちらは、トークンを使います。
まずはビューから確認していきましょう。
 
トークンをoutput()とすると、hiddenタイプのinputタグが生成されます。
 
Screen Shot 2021-10-15 at 9.01.56.png
 
開発ツールでも確認できますね。
Screen Shot 2021-10-15 at 9.03.21.png
 
 
<?=Core::make('token')->output('go_to_conf');?>
ビュー側で生成したトークンをコントローラー側で確認します。
「go_to_conf」という名前でトークンを設定したので、ここでも同じ「go_to_conf」としてください。

Screen Shot 2021-10-14 at 16.46.16.png

自動的にトークンがチェックされ、トークンが一致しなければフォームの送信自体ができなくなります。
設定箇所が少ないので、簡単にセキュリティ対策できますね。
忘れずに実装するようにしてください。
 
 //トークン
 $token = \Core::make("token")
 //真偽値チェック
 $token->validate('go_to_conf')
セキュリティ対策も含め、全て実行したものがこちらになります。
 
コントローラー
<?php

namespace Application\Block\ExternalForm\Form\Controller;

use Concrete\Core\Controller\AbstractController;

class FoobarForm extends AbstractController
{

    public function view()
    {
        $this->set('message', h('Edit Page'));
        $this->set('mode', 'edit');
    }

    public function action_go_to_conf($bID = false)
    {
        $token = \Core::make("token");
        if ($this->bID == $bID && $token->validate('go_to_conf')) {
            $this->set('message', h('Conf Page'));
            $fooBarTextField = $this->get('foo_bar_text_field');
            $input['foo_bar_text_field'] = $fooBarTextField;
            $this->set('input', $input);
            $this->set('mode', h('conf'));
            return true;
        }
    }

    public function action_go_to_comp($bID = false)
    {
        if ($this->bID == $bID) {
            $this->set('message', h('Comp Page'));
            $this->set('response', h('Thanks!'));
            $this->set('mode', h('comp'));
            return true;
        }
    }

    public function action_back_to_edit($bID = false)
    {
        if ($this->bID == $bID) {
            $this->set('mode', h('edit'));
            return true;
        }
    }
}
ビュー
<?php
$form = Loader::helper('form');
defined('C5_EXECUTE') or die("Access Denied.");
if (isset($response)) {
    ?>
    <div class="alert alert-info"><?= h($response) ?></div>
    <?php
} ?>
<?php
if ($mode === 'edit') {
    ?>
    <!--入力画面-->
    <form method="post" action="<?= h($view->action('go_to_conf')) ?>">
        <?=Core::make('token')->output('go_to_conf');?>
        <p style="font-size: 20px;font-weight: 700; color: darkgoldenrod;"><?= $message ?></p>
        <div class="form-group">
            <label class="control-label"><?= h('Text Field') ?></label>
            <?= $form->text('foo_bar_text_field') ?>
        </div>
        <div class="form-group">
            <input type="submit" name="submit" value="submit" class="btn btn-default"/>
        </div>
    </form>
<?php } else if ($mode === 'conf') { ?>
    <!--確認画面-->
    <p style="font-size: 20px;font-weight: 700; color: darkgoldenrod;"><?= h($message) ?></p>
    <p>Your input is <span style="font-size:20px; font-weight:700;"><?= h($input['foo_bar_text_field']) ?></span>.</p>
    <div style="display:flex;">
        <form method="post" action="<?= h($view->action('go_to_comp')) ?>" style="margin-right: 10px;">
            <div class="form-group">
                <input type="submit" name="submit" value="submit" class="btn btn-default"/>
            </div>
        </form>
        <form method="post" action="<?= h($view->action('back_to_edit')) ?>" class="test">
            <div class="form-group">
                <?= $form->hidden('foo_bar_text_field', h($input['foo_bar_text_field'])) ?>
                <div class="form-group">
                    <input type="submit" name="submit" value="back" class="btn btn-default"/>
                </div>
            </div>
        </form>
    </div>
<?php } else {
    ?>
    <!--完了画面-->
    <p style="font-size: 20px;font-weight: 700; color: darkgoldenrod;"><?= h($message) ?></p>
<?php }
入力画面

Screen Shot 2021-10-08 at 18.01.37.png

確認画面

Screen Shot 2021-10-08 at 18.01.43.png

完了画面

Screen Shot 2021-10-08 at 18.01.52.png

終わりに

想定通りの動きになったでしょうか?
Concrete CMSで用意されている通常のフォームブロックと違い、外部フォームブロックではPHPでの実装がベースとなっています。
今回は、テキストフィールドを持つフォームの入力・確認・管理画面を作成しましたが、次回はもう少し他の機能も追加してみたいと思います。