Python 軽量フレームワーク Flask  と CSRF Protection を使う

# 課題
* Web サイト上で問い合わせを受け付ける、問い合わせ Web アプリケーションを作成する。「入力フォーム」「入力確認画面」「送信完了画面」の3画面からなり、少なくとも氏名・メールアドレス・問い合わせ内容を記入するものとします。
* 開発言語や動作環境を含め、その他の仕様は指定しませんので、自由に作成してください。

## フレームワーク選択 
Python の軽量フレームワーク Flask を選択。

## 画面遷移と機能
* index.html （メニュー）→ register.html （入力）→ confirm.html （確認）→ finish.html （完了）
* どうセキュアにつくるかが課題なので、データベースへの書き込みなど実務面の機能はなし。

## 留意した点

* 車輪を発明しない。フレームワーク内の機能を使う、具体的には
 * CSRF Protect
 * html エスケープ
 * email アドレスなど入力値 validate
 * セッション管理
 * テンプレートとプログラムの分離

### CSRF protect
* 入力 → 確認 → 終了 一連の流れでは、横からブラウザで同画面をアクセスしても許可しない。

### セッション管理
* クライアント（Web ブラウザ）の cookie でもたせるのはキー値のみ。クライアントのキー値をもとに cookie の中身はすべてサーバー側でもつ
* 更にサーバーでもつ cookie の中身は暗号化される

### html エスケープ
* お問い合わせの textbox のみ改行（\r, \r\n）のみ \<br/\> に変形し、確認時の視認性を高める。
* 他はフレームワーク側で文脈により自動で html エスケープがかかる（例えば、textbox 再入力の際の値入れは html エスケープをかけない）。

## 動作環境
### Python + Windows10
```shell-session
> python --version
Python 2.7.14
```
https://www.python.org/ でダウンロード c:\Python27 にインストール。cygwin の python だといろいろうまく動かないと思われる。

### PyCharm Professional
例えば email アドレスの validator があるなんてどこにも書いていないので、試行錯誤するときは PyCharm のサジェスチョン機能が重宝されます。関数のあとは二行あけろとか細かく指定されるので体で Python のお作法を覚えることができます。
メールアドレスで ac.jp を持っている人は一年間タダになります。無料の community edition もありますがどこまでつかえるのかは未確認です。
ダウンロードはこちらから 201801 Professional https://www.jetbrains.com/pycharm/

## ディレクトリ構成
```
+--venv （Python Virtual Env PyCharm より自動生成）
|  |--Include
|  |--Lib
|  |--Scripts
|  +--tcl
+--www
   |--app.py
   |--flask_session （flask_session により自動生成）
   |  |--0aff4f53f81709b8a8d38705b0b1b093 （自動生成）
   |  |--2029240f6d1128be89ddc32729463129 （自動生成）
   |  +--a97766e7f7198c8c4ea4750802c0286a （自動生成）
   +--templates
      |--_formhelpers.html （入力画面ヘルパー）
      |--confirm.html
      |--csrf_error.html （CSRF Protect のエラー画面）
      |--finish.html
      |--index.html
      +--register.html
```

## Python 拡張モジュールインストール
以下は Flask だけですが、他にも必要なので app.py の import ～ from の行を参照して入れてください。モジュールが入っていなければ Pycharm でうねうね下線が入ってます。
PyCharm の Teminal 画面より

```shell-session
> pip install Flask
```

## 起動
PyCharm の Teminal 画面より

```shell-session
> python app.py
```

## 閲覧
Web ブラウザから
http://localhost:5000/

## ソース
```python:app.py
# -*- coding: utf-8 -*-
from wtforms import BooleanField, StringField, PasswordField, validators
from wtforms.widgets import TextArea
from flask import render_template, redirect, request, url_for, Flask, session
from flask_session import Session
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect, CSRFError
import re
from jinja2 import evalcontextfilter, Markup, escape

app = Flask(__name__)
sess = Session()
csrf = CSRFProtect(app)

_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}')

app = Flask(__name__)


@app.template_filter()
@evalcontextfilter
# http://flask.pocoo.org/snippets/28/
def nl2br(eval_ctx, value):
    result = u'\n\n'.join(u'<p>%s</p>' % p.replace('\n', '<br/>\n') for p in _paragraph_re.split(escape(value)))
    if eval_ctx.autoescape:
        result = Markup(result)
    return result


class RegistrationForm(FlaskForm):  # FlaskForm にしないと CSRF Protect が有効にならない
    username = StringField(u"ユーザー名", [validators.DataRequired(), validators.Length(min=4, max=25)])
    email = StringField(u"メールアドレス", [validators.DataRequired(), validators.Email()])
    question = StringField(u"お問い合わせ内容", validators=[validators.DataRequired()],
                           widget=TextArea())
    password = PasswordField(u"パスワード", [
        validators.DataRequired(),
        validators.EqualTo('confirm', message=u"パスワードが一致しません")
    ])
    confirm = PasswordField(u"パスワード確認")
    accept_tos = BooleanField(u"私は規約に同意します", [validators.DataRequired()])


class ConfirmationForm(FlaskForm):
    accept_confirm = BooleanField(u"この内容で登録する（チェックなしはやり直し）", [validators.DataRequired()])


@app.errorhandler(CSRFError)
def handle_csrf_error(e):
    return render_template('csrf_error.html', reason=e.description), 400


@app.route('/', methods=['GET'])
def index():
    session.clear()
    return render_template('index.html', register_url=url_for('register'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    register_form = RegistrationForm(request.form,
                                     username=session.get('username', ''), email=session.get('email', ''),
                                     question=session.get('question', ''))
    # このトークンの正当性はform.validate_on_submit()で検証します
    if request.method == 'POST' and register_form.validate():
        session['username'] = request.form.get('username')
        session['email'] = request.form.get('email')
        session['password'] = request.form.get('password')
        session['question'] = request.form.get('question')
        confirm_form = ConfirmationForm(request.form)
        return render_template('confirm.html', username=session['username'], email=session['email'],
                               question=session['question'],
                               form=confirm_form, action_for=url_for('finish'), submit_value=u"よろしいです")
    return render_template('register.html', form=register_form, submit_value=u"入力内容を確認")


@app.route('/finish', methods=['POST'])
def finish():
    confirm_form = ConfirmationForm(request.form)
    if request.method == 'POST' and confirm_form.validate():
        username = session.get('username')
        session.clear()
        return render_template('finish.html', username=username,
                               message=u"ご登録ありがとうございました。")
    return redirect(url_for('register'))


if __name__ == "__main__":
    app.debug = True
    app.config.update(dict(
        SECRET_KEY="powerful secret key",
        WTF_CSRF_SECRET_KEY="a csrf secret key",
        SESSION_TYPE='filesystem'
    ))
    sess.init_app(app)
    csrf.init_app(app)
    app.run()
```
```html:_formhelpers.html
{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}
```
```html:confirm.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>confirm</title>
</head>
<body>
{% from "_formhelpers.html" import render_field %}
<form action="{{ action_for }}" method=post>
  {{ form.csrf_token }}
  <dl>
    ユーザー名: {{ username }}<br/>
    パスワード: {{ email }}<br/>
    お問い合わせ内容: {{ question | nl2br }}<br/>
    {{ render_field(form.accept_confirm) }}
  </dl>
  <p><input type=submit value={{submit_value}}>
</form>
</body>
```
```html:csrf_error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>csrf_error</title>
</head>
<body>
csrf error page
</body>
</html>
```
```html:finish.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>finish</title>
</head>
<body>
{{ username }}様 {{message}}
</body>
```
```html:index.html
<!DOCTYPE html>
<html lang="ja>
<head>
    <meta charset="UTF-8">
    <title>メニュー</title>
</head>
<body>
Привет!
<a href="{{register_url}}">登録</a>
</body>
</html>
```
```html:register.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>register</title>
</head>
<body>
{% from "_formhelpers.html" import render_field %}
<form method=post>
  {{ form.csrf_token }}
  <dl>
    {{ render_field(form.username, size=40) }}
    {{ render_field(form.email, size=40) }}
    {{ render_field(form.password, size=40) }}
    {{ render_field(form.confirm, size=40) }}
    {{ render_field(form.question, rows=10, cols=40) }}
    {{ render_field(form.accept_tos) }}
  </dl>
  <p><input type=submit value={{submit_value}}>
</form>
```