BEAR.Sundayでコンタクトフォームを作ってみる②

BEAR.Sundayでコンタクトフォームを作ってみる①の続きです。

いよいよフォームの作成に入ります。

コンタクトフォームのテンプレートの作成

ページリソースContact用のTwigテンプレートを作成します。

src/Resource/Page/Contact.twig

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>BEAR.Sunday Contact Form</title>
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <h1>Contact Form</h1>

{% if code == 201 %}
    <p>
    Name: {{ name }}<br>
    Email: {{ email }}<br>
    Comment: {{ comment }}
    </p>
    <p>Thank you!</p>
{% else %}
    <form role="form" action="" method="post" enctype="multipart/form-data">
        <div class="form-group{%if form['name']['error'] %} has-error{% endif %}">
            <label class="control-label" for="name">Name</label>
            <input id="name" type="text" name="name" class="form-control" size="20" maxlength="50" value="{{ form['name']['value'] }}" />
            <label class="control-label" for="name">{{ form['name']['error'] }}</label>
        </div>

        <div class="form-group{%if form['email']['error'] %} has-error{% endif %}">
            <label class="control-label" for="email">Email</label>
            <input id="email" type="text" name="email" class="form-control" size="20" maxlength="100" value="{{ form['email']['value'] }}" />
            <label class="control-label" for="email">{{ form['email']['error'] }}</label>
        </div>

        <div class="form-group{%if form['comment']['error'] %} has-error{% endif %}">
            <label class="control-label" for="comment">Comment</label>
            <textarea id="comment" name="comment" class="form-control" cols="40" rows="5">{{ form['comment']['value'] }}</textarea>
            <label class="control-label" for="comment">{{ form['comment']['error'] }}</label>
        </div>

        <input class="btn btn-default" type="submit" name="submit" value="Send">
    </form>
{% endif %}

</div>
</body>
</html>

codeの値により、送信完了時と送信前のフォームの表示を切り替えています。

ページコントローラの作成

ページコントローラContactを作成します。

以下のように変更します。

src/Resource/Page/Contact.php

<?php
/**
 * Kenjis.Contact
 *
 * @author     Kenji Suzuki <https://github.com/kenjis>
 * @license    MIT License
 * @copyright  2014 Kenji Suzuki
 * @link       https://github.com/kenjis/Kenjis.Contact
 */

namespace Kenjis\Contact\Resource\Page;

use BEAR\Resource\ResourceObject;
use Ray\Di\Di\Inject;
use Kenjis\Contact\Service\SwiftMailerFactory;
use BEAR\Resource\Code;

class Contact extends ResourceObject
{
    /**
     * @Inject
     */
    public function __construct(SwiftMailerFactory $mailer)
    {
        $this->mailer = $mailer;
    }

    public function onGet()
    {
        return $this;
    }

    public function onPost($name, $email, $comment)
    {
        if (! $this->validation($name, $email, $comment)) {
            return $this;
        }

        $this->sendmail($name, $email, $comment);

        $this['code'] = $this->code = Code::CREATED;
        $this['name']    = $name;
        $this['email']   = $email;
        $this['comment'] = $comment;

        return $this;
    }

    private function validation($name, $email, $comment)
    {
        $pass = true;

        if ((mb_strlen($name) == 0) || (mb_strlen($name) > 50)) {
            $this->body['form']['name']['error'] = 'Enter your name (max 50 letters).';
            $pass = false;
        }

        if ((mb_strlen($email) > 100) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->body['form']['email']['error'] = 'Enter your email adrress (max 100 letters).';
            $pass = false;
        }

        if ((mb_strlen($comment) == 0) || (mb_strlen($comment) > 400)) {
            $this->body['form']['comment']['error'] = 'Enter comment (max 400 letters).';
            $pass = false;
        }

        if (! $pass) {
            $this->body['form']['name']['value']    = $name;
            $this->body['form']['email']['value']   = $email;
            $this->body['form']['comment']['value'] = $comment;
        }

        return $pass;
    }

    private function sendmail($name, $email, $comment)
    {
        $data = [
            'name'    => $name,
            'email'   => $email,
            'comment' => $comment,
        ];

        $mailer = $this->mailer->create();
        $mailer->setSubject('コンタクトフォーム')
            ->setFrom($data['email'], $data['name'])
            ->setTo('admin@example.org', '管理者')
            ->setTemplate('mailer/contact_form.twig', $data);

//        echo '<pre>'
//            . htmlspecialchars($mailer, ENT_QUOTES, 'UTF-8')
//            . '</pre>';

        $result = $mailer->send();
        return $result;
    }
}

POST時に実行されるonPost()メソッドを追加し、入力データの検証をvalidation()メソッドに、メール送信処理はsendmail()メソッドに分離しています。

これで、http://0.0.0.0:8000/contact にブラウザからアクセスすると、次のようなコンタクトフォームが表示されます。

▼コンタクトフォーム

何も入力せずに[Send]ボタンを押すと、検証エラーが表示されます。

▼検証エラー

入力してみます。

▼コンタクトフォーム入力中

[Send]ボタンを押すと、今度は送信されたようです。

▼送信完了

おっと、「check」に取り消し線が引かれています。HTMLタグが有効になってしまっています。

つまり、このコンタクトフォームにはXSS脆弱性があるということになります。ついでに、CSRF対策もしてませんが、今日はここまでにします。

BEAR.Sundayでコンタクトフォームを作ってみる③へ続く。

過去記事

関連

Tags: bear

BEAR.Sundayでコンタクトフォームを作ってみる①

BEAR.Sundayでの「Hello World!」①がGoogle検索「BEAR.Sunday Hello World」で1位をゲットしました!

ということで、そろそろBEAR.SundayのHello Worldにも飽きてきたと思いますので、先に進みたいと思います。

次のお題はコンタクトフォームです。

BEAR.Sundayのインストール

bear/skeletonを使ってインストールします。最新の開発版をインストールするためにdev-developを指定します。

$ composer create-project bear/skeleton:dev-develop Kenjis.Contact
$ cd Kenjis.Contact
$ composer install

依存パッケージの更新と追加

bear/packageをdevelopブランチに、ray/diをmasterブランチに、そして、メール送信のためにSwiftMailerを追加します。

--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,9 @@
     "description":"n/a",
     "license": "proprietary",
     "require": {
-        "bear/package": "~0.11"
+        "bear/package": "dev-develop",
+        "ray/di": "dev-master",
+        "swiftmailer/swiftmailer": "@stable"
     },
     "require-dev": {
         "bear/dev-package": "~0.1@dev"
$ composer update

SwiftMailerを使う準備

SwiftMailerクラスの作成

そのままSwiftMailerを使うこともできますが、ここでは、より簡単に使えるように、Twigと合わせてテンプレートを使えるようにしたSwiftMailerクラスを作成しておきます。

src/Service/SwiftMailer.php

<?php
/**
 * Kenjis.Contact
 *
 * @author     Kenji Suzuki <https://github.com/kenjis>
 * @license    MIT License
 * @copyright  2014 Kenji Suzuki
 * @link       https://github.com/kenjis/Kenjis.Contact
 */

namespace Kenjis\Contact\Service;

class SwiftMailer
{
    private $mailer;
    private $twig;

    private $subject;
    private $from;
    private $to;

    private $template;
    private $templateVars;

    public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig)
    {
        $this->mailer = $mailer;
        $this->twig = $twig;

        // logger for debug
//        $logger = new \Swift_Plugins_Loggers_EchoLogger();
//        $this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($logger));
    }

    /**
     * @return \Swift_Message
     */
    private function build()
    {
        $body = $this->twig->render($this->template, $this->templateVars);

        $message = \Swift_Message::newInstance()
            ->setSubject($this->subject)
            ->setFrom($this->from)
            ->setTo($this->to)
            ->setBody($body);

        return $message;
    }

    /**
     * @param string $template template filename
     * @param array $vars variables to pass template
     */
    public function setTemplate($template, array $vars)
    {
        $this->template = $template;
        $this->templateVars = $vars;
        return $this;
    }

    public function setSubject($subject)
    {
        $this->subject = $subject;
        return $this;
    }

    public function setFrom($address, $name)
    {
        $this->from = [$address => $name];
        return $this;
    }

    public function setTo($address, $name)
    {
        $this->to = [$address => $name];
        return $this;
    }

    /**
     * Send mail
     *
     * @return int the number of recipients who were accepted for delivery
     */
    public function send()
    {
        $message = $this->build();
        return $this->mailer->send($message);
    }

    /**
     * Get this message as a complete string
     *
     * @return string
     */
    public function __toString()
    {
        return $this->build()->toString();
    }
}

SwiftMailerFactoryクラスの作成

上のSwiftMailerクラスを生成するファクトリも作成しておきます。

src/Service/SwiftMailerFactory.php

<?php
/**
 * Kenjis.Contact
 *
 * @author     Kenji Suzuki <https://github.com/kenjis>
 * @license    MIT License
 * @copyright  2014 Kenji Suzuki
 * @link       https://github.com/kenjis/Kenjis.Contact
 */

namespace Kenjis\Contact\Service;

use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;

class SwiftMailerFactory
{
    /**
     * @var string
     */
    private $context;

    private $twig;

    /**
     * @Inject
     * @Named("context=app_context")
     */
    public function __construct($context)
    {
        $this->context = $context;
    }

    /**
     * @Inject
     */
    public function setTwig(\Twig_Environment $twig)
    {
        $this->twig = $twig;
    }

    /**
     * @return \Swift_Mailer
     */
    public function create()
    {
        if ($this->context === 'test') {
            $transport = \Swift_NullTransport::newInstance();
        } else {
            $transport = \Swift_SmtpTransport::newInstance('smtp.gmail.com', '465', 'ssl')
                ->setUsername($_ENV['MAILER_GMAIL_ID'])
                ->setPassword($_ENV['MAILER_GMAIL_PASSWORD']);
        }

        $mailer = \Swift_Mailer::newInstance($transport);

        return new SwiftMailer($mailer, $this->twig);
    }
}

コンストラクタインジェクション(@Inject)で名前付きバインディング(@Named)を使い、$contextにBEAR.Sundayのapp_context(アプリケーションコンテキスト、いわゆる環境。デフォルトではprod、dev、test、apiのいずれか)を注入しています。

setTwig()メソッドでは、セッターインジェクション(@Inject)を使い、$twigにTwig_Environmentインスタンスを注入しています。

create()メソッドではコンテキストにより、SwiftMailerのトランスポートオブジェクトを変更しています。test環境ではSwift_NullTransportを使いメールの転送を行いません。test環境以外ではGmailをSMTPサーバに利用します。

メール用のテンプレートの作成

作成したSwiftMailerクラスで使うメール用のテンプレートを作成します。

var/lib/twig/template/mailer/contact_form.twig

{% autoescape false %}
名前: {{ name }}
電子メール: {{ email }}
コメント:
{{ comment }}
{% endautoescape %}

サーバ起動スクリプトの作成

GmailをSMTPサーバとして利用するにはGmailアカウントの情報が必要です。

ここでは、以下のシェルスクリプトを作成し、環境変数でGmailアカウントの情報をWebサーバに渡すようにします。

server.sh

#!/bin/sh

export MAILER_GMAIL_ID=アカウント@gmail.com
export MAILER_GMAIL_PASSWORD=パスワード

# clear BEAR.Sunday's cache
php bin/clear.php

vendor/bin/bear.server --port=8000 --context=dev .
#php -S 0.0.0.0:8000 -t var/www/ bootstrap/contexts/dev.php

メール送信のテスト

SwiftMailerを使う準備ができましたので、メール送信をテストしてみます。

とりあえず、ページリソースContactを作成し、GETされた場合にメールを送信してみます。

src/Resource/Page/Contact.php

<?php
/**
 * Kenjis.Contact
 *
 * @author     Kenji Suzuki <https://github.com/kenjis>
 * @license    MIT License
 * @copyright  2014 Kenji Suzuki
 * @link       https://github.com/kenjis/Kenjis.Contact
 */

namespace Kenjis\Contact\Resource\Page;

use BEAR\Resource\ResourceObject;
use Ray\Di\Di\Inject;
use Kenjis\Contact\Service\SwiftMailerFactory;

class Contact extends ResourceObject
{
    /**
     * @Inject
     */
    public function __construct(SwiftMailerFactory $mailer)
    {
        $this->mailer = $mailer;
    }

    public function onGet()
    {
        $data = [
            'name'    => 'BEAR.Sunday',
            'email'   => 'bear@example.jp',
            'comment' => 'テストメールです。',
        ];

        $mailer = $this->mailer->create();
        $mailer->setSubject('テストメール')
            ->setFrom($data['email'], $data['name'])
            ->setTo('admin@example.org', '管理者')
            ->setTemplate('mailer/contact_form.twig', $data);

        echo '<pre>'
            . htmlspecialchars($mailer, ENT_QUOTES, 'UTF-8')
            . '</pre>';

        $result = $mailer->send();
        return (string) $result;
    }
}

デバッグのために$mailerをechoしてメッセージの文字列を表示しています。 また、SwiftMailerのsend()メソッドは処理したメールの数(整数)を返しますので、それを文字列にキャストして返して表示します。

サーバ起動スクリプトを実行し、PHPのビルトインサーバを起動します。

$ sh server.sh

これで、http://0.0.0.0:8000/contact にブラウザからアクセスすると、Gmail経由でメールが送信されるはずです。

うまくいかない場合は、SwiftMailerクラスのコンストラクタの最後にあるEchoLoggerプラグインを登録するコードを有効にすると、SMTPサーバとのやりとりのログがechoされるようになります。

src/Service/SwiftMailer.php

    public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig)
    {
        $this->mailer = $mailer;
        $this->twig = $twig;

        // logger for debug
//        $logger = new \Swift_Plugins_Loggers_EchoLogger();
//        $this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($logger));
    }

というわけで、メール送信だけで終わってしまいましたが、今日はここまでにします。

BEAR.Sundayでコンタクトフォームを作ってみる②へ続く。

関連

Tags: bear, swiftmailer

FuelPHP 1.7.2のブログチュートリアル③

ブログチュートリアル①ブログチュートリアル②を合わせて、よりブログっぽいものにしてみます。

【注意】このチュートリアルはアプリ作成の最短の手順を示したものであり、セキュリティ上必要な設定や機能が省略されています。実際にアプリを運用する場合は、『はじめてのフレームワークとしてのFuelPHP第2版(3) 実践編』などを参考に必要なセキュリティ上の設定や機能をすべて実装されることをお薦めします。

FuelPHP 1.7.2のインストール設定

http://fuelphp.com/の「Download v1.7.2 now!」より、fuelphp-1.7.2.zipをダウンロードし展開します。

config.phpの設定

FuelPHPの設定ファイルfuel/app/config/config.phpを変更し、FuelPHPのORMパッケージとAuthパッケージを使えるようにします。

--- a/fuel/app/config/config.php
+++ b/fuel/app/config/config.php
@@ -258,7 +258,7 @@ return array(
        /**************************************************************************/
        /* Always Load                                                            */
        /**************************************************************************/
-       // 'always_load'  => array(
+       'always_load'  => array(

                /**
                 * These packages are loaded on Fuel's startup.
@@ -271,9 +271,10 @@ return array(
                 *     array('auth'     => PKGPATH.'auth/')
                 * );
                 */
-               // 'packages'  => array(
-               //      //'orm',
-               // ),
+               'packages'  => array(
+                       'orm',
+                       'auth',
+               ),

                /**
                 * These modules are always loaded on Fuel's startup. You can specify them
@@ -309,6 +310,6 @@ return array(
                 * If you don't want the lang in a group use null as groupname.
                 */
                // 'language'  => array(),
-       // ),
+       ),

 );

配列のキーalways_loadpackagesormが有効になるように、コメント記号//を削除し、ormの下にauthを追加します。

auth.phpの設定

Authパッケージの設定ファイルfuel/packages/auth/config/auth.phpをfuel/app/config/にコピーし、driverをOrmauthに変更します。

--- fuel/packages/auth/config/auth.php  2014-07-22 18:23:20.000000000 +0900
+++ fuel/app/config/auth.php    2014-07-23 20:07:04.125328123 +0900
@@ -22,7 +22,7 @@
  */

 return array(
-   'driver' => 'Simpleauth',
+   'driver' => 'Ormauth',
    'verify_multiple_logins' => false,
    'salt' => 'put_your_salt_here',
    'iterations' => 10000,

データベースの準備

MySQL にデータベースを作成します。

> CREATE DATABASE `fuel_blog` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

fuel/app/config/development/db.phpを変更し、FuelPHPからデータベースにアクセスできるようにします。

return array(
    'default' => array(
        'connection'  => array(
            'dsn'        => 'mysql:host=localhost;dbname=fuel_blog',
            'username'   => 'root',
            'password'   => '',
        ),
    ),
);

ブログの作成

FuelPHPのoil generateコマンドによりコードを自動生成します。管理ページの方は--skipオプションを付けてすでに存在するファイルの生成をスキップします。

$ php oil generate scaffold post title:varchar[50] body:text
$ php oil generate admin post title:varchar[50] body:text --skip

【注意】生成された全てのコードに目を通し、問題がないか確認することをお薦めします。

マイグレーションを実行し、データベースにテーブルを作成します。

$ php oil refine migrate
$ php oil refine migrate --packages=auth

これで、http://localhost:8000/postに認証なしのCRUDページが、http://localhost:8000/admin/postに認証付きのCRUDページが作成されました。

ソースコードの変更

http://localhost:8000/postの方は、ブログの一覧表示と個別の記事の表示があればよいので、それ以外の機能を削除します。

Postコントローラから、create、edit、deleteのアクションを削除します。

fuel/app/classes/controller/post.php

--- a/fuel/app/classes/controller/post.php
+++ b/fuel/app/classes/controller/post.php
@@ -24,109 +24,4 @@ class Controller_Post extends Controller_Template
                $this->template->content = View::forge('post/view', $data);

        }
-
-       public function action_create()
-       {
-               if (Input::method() == 'POST')
-               {
-                       $val = Model_Post::validate('create');
-
-                       if ($val->run())
-                       {
-                               $post = Model_Post::forge(array(
-                                       'title' => Input::post('title'),
-                                       'body' => Input::post('body'),
-                               ));
-
-                               if ($post and $post->save())
-                               {
-                                       Session::set_flash('success', 'Added post #'.$post->id.'.');
-
-                                       Response::redirect('post');
-                               }
-
-                               else
-                               {
-                                       Session::set_flash('error', 'Could not save post.');
-                               }
-                       }
-                       else
-                       {
-                               Session::set_flash('error', $val->error());
-                       }
-               }
-
-               $this->template->title = "Posts";
-               $this->template->content = View::forge('post/create');
-
-       }
-
-       public function action_edit($id = null)
-       {
-               is_null($id) and Response::redirect('post');
-
-               if ( ! $post = Model_Post::find($id))
-               {
-                       Session::set_flash('error', 'Could not find post #'.$id);
-                       Response::redirect('post');
-               }
-
-               $val = Model_Post::validate('edit');
-
-               if ($val->run())
-               {
-                       $post->title = Input::post('title');
-                       $post->body = Input::post('body');
-
-                       if ($post->save())
-                       {
-                               Session::set_flash('success', 'Updated post #' . $id);
-
-                               Response::redirect('post');
-                       }
-
-                       else
-                       {
-                               Session::set_flash('error', 'Could not update post #' . $id);
-                       }
-               }
-
-               else
-               {
-                       if (Input::method() == 'POST')
-                       {
-                               $post->title = $val->validated('title');
-                               $post->body = $val->validated('body');
-
-                               Session::set_flash('error', $val->error());
-                       }
-
-                       $this->template->set_global('post', $post, false);
-               }
-
-               $this->template->title = "Posts";
-               $this->template->content = View::forge('post/edit');
-
-       }
-
-       public function action_delete($id = null)
-       {
-               is_null($id) and Response::redirect('post');
-
-               if ($post = Model_Post::find($id))
-               {
-                       $post->delete();
-
-                       Session::set_flash('success', 'Deleted post #'.$id);
-               }
-
-               else
-               {
-                       Session::set_flash('error', 'Could not delete post #'.$id);
-               }
-
-               Response::redirect('post');
-
-       }
-
 }

一覧ページのビューファイルから、Edit、DeleteそしてAdd new Postボタンを削除します。

fuel/app/views/post/index.php

--- a/fuel/app/views/post/index.php
+++ b/fuel/app/views/post/index.php
@@ -17,7 +17,8 @@
            <td>
                <div class="btn-toolbar">
                    <div class="btn-group">
-                       <?php echo Html::anchor('post/view/'.$item->id, '<i class="icon-eye-open"></i> View', array('class' => 'btn btn-small')); ?>                        <?php echo Html::anchor('post/edit/'.$item->id, '<i class="icon-wrench"></i> Edit', array('class' => 'btn btn-small')); ?>                      <?php echo Html::anchor('post/delete/'.$item->id, '<i class="icon-trash icon-white"></i> Delete', array('class' => 'btn btn-small btn-danger', 'onclick' => "return confirm('Are you sure?')")); ?>                 </div>
+                       <?php echo Html::anchor('post/view/'.$item->id, '<i class="icon-eye-open"></i> View', array('class' => 'btn btn-small')); ?>
+                   </div>
                </div>

            </td>
@@ -28,7 +29,4 @@
 <?php else: ?>
 <p>No Posts.</p>

-<?php endif; ?><p>
-   <?php echo Html::anchor('post/create', 'Add new Post', array('class' => 'btn btn-success')); ?>
-
-</p>
+<?php endif; ?>

個別の記事ページのビューファイルからEditリンクを削除します。

fuel/app/views/post/view.php

--- a/fuel/app/views/post/view.php
+++ b/fuel/app/views/post/view.php
@@ -7,5 +7,4 @@
        <strong>Body:</strong>
        <?php echo $post->body; ?></p>

-<?php echo Html::anchor('post/edit/'.$post->id, 'Edit'); ?> |
-<?php echo Html::anchor('post', 'Back'); ?>
\ No newline at end of file
+<?php echo Html::anchor('post', 'Back'); ?>

Webサーバの起動

PHP 5.4以降のビルトインWebサーバを起動します。

$ php oil server

ブラウザからhttp://localhost:8000/postにアクセスすると、記事の一覧が表示されます。

スクリーンショット

ブラウザからhttp://localhost:8000/admin/postにアクセスすると、ログインページにリダイレクトされます。

デフォルトの管理者ユーザでログインできます。

  ユーザ名:admin
パスワード:admin

http://localhost:8000/admin/postにアクセスすると、記事の一覧が表示され、記事の追加、削除、編集ができます。

スクリーンショット

Tags: fuelphp, database