$yuzu->log();

技術ネタなど。

【CentOS】迷惑メール(スパム)扱いされない為の最低限設定しておきたい3つの設定【Postfix】

さくらVPSCentOSの環境を構築し、localhost(ローカルホスト)からPostfixでメールを送信しても、なにも設定していないと高確率で迷惑メール(スパム)扱いされてしまいます。
Yahoo!メールは幾分か緩いですが、Gmailはほぼ100%迷惑メール(スパム)扱いされます。

最近は無料でSMTPサーバを貸してくれるサービスもあり、そちらを使う場合もあるかもしれませんが、localhost(ローカルホスト)から送信する場合もあると思うので、そちらで最低限しておきたい3つの設定を記します。

当方の環境は下記の通りです。

サーバー: さくらVPS(CentOS)
ドメイン: お名前.comで契約したhoge.com
メールソフト: Postfix

1. DNS逆引きレコード変更

さくらVPSの場合、契約したタイミングで、さくらのドメインが割り振られますが、WEBサービスを運営する場合は独自ドメインを取得し、そちらで運用してると思います。

何も設定せずにサーバーのIPを逆引きした場合、さくらのドメインが表示されるため、運用しているWEBサービスドメインと異なり、迷惑メール(スパム)扱いされます。

ですので、まずはDNS逆引きレコードを運営しているWEBサービスと同じドメインに変更しましょう。

さくらVPSの場合、コントロールパネルから逆引き設定できます。 f:id:yuzurus:20150613144542p:plain

確認は以下のコマンドを叩いて、ドメインが変わっていればOK

$ nslookup IPアドレス

2. SPFレコードの設定

Sender Policy Framework(センダー・ポリシー・フレームワーク)とは、電子メールにおける送信ドメイン認証のひとつ。差出人のメールアドレスが他のドメインになりすましていないかどうかを検出することができる。 SPF もしくは SPF認証 とも呼ばれる。

端的に言えば設定しておけば成りすましじゃないよ、ということを伝えることができるので、SPFレコードを設定してください。

お名前.comの場合は下記の画像のようにしてください。 f:id:yuzurus:20150613142310p:plain

ホスト名: 空
TYPE: TXT
TTL: 3600(お好みで)
VALUE: v=spf1 +a:hoge.com ~all

DKIMの設定

Domainkeys Identified Mail(DKIM)は、電子署名方式の送信ドメイン認証である。 DKIMでは送信側で電子メールに電子署名を付加し、受信側でその電子署名を照合するという方法で送信者のドメイン認証を行う。

DKIMの設定はOpenDKIMを使います。

sudo yum install opendkim

OpenDKIMを使って鍵を作成します。

sudo opendkim-genkey -D /etc/opendkim/keys/ -s hoge_com_selector -d hoge.com

作成された/etc/opendkim/keys/hoge_com_selector.txt の内容をDNSに登録してください。

f:id:yuzurus:20150613150429p:plain ホスト名: hoge_com_selector._domainkey
TYPE: TXT
TTL: 3600(お好みで)
VALUE: v=DKIM1; k=rsa; p=MIGfMA0GCSq…

/etc/opendkim.confの設定

# 送信と受信
Mode sv

# comment out
# KeyFile /etc/opendkim/keys/default.private

# uncomment
KeyTable refile:/etc/opendkim/KeyTable

# uncomment
SigningTable refile:/etc/opendkim/SigningTable

# uncomment
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts

# uncomment
InternalHosts refile:/etc/opendkim/TrustedHosts 

/etc/opendkim/KeyTableの設定

hoge_com_selector._domainkey.hoge.com hoge.com:hoge_com_selector:/etc/opendkim/keys/hoge_com_selector.private

/etc/opendkim/SigningTableの設定

*@hoge.com hoge_com_selector._domainkey.hoge.com

Postfixの設定

/etc/postfix/main.cfの末尾に下記を追記

smtpd_milters = inet:127.0.0.1:8891

OpenDKIMとPostfixのrestart

sudo service opendkim restart
sudo service postfix restart

OpenDKIMの設定が正しいか確認

メールを送信して署名が追加されているか確認する。

# mail example@gmail.com
Subject: Test
Test
.

メールログ(/var/log/maillog)を確認し、

DKIM-Signature header added (s=hoge_com_selector, d=hoge.com)

という文字があれば設定完了。

参考 http://nobnoob.hatenablog.com/entry/2013/04/01/093120

【チュートリアル】最新フレームワークCakePHP3でブックマーカーを作ってみる その2

yuzurus.hatenablog.jp

前回の続きになります。

ログイン機能の実装

ログイン機能はCakePHP2同様AuthComponentを利用します。

AppContorollerに追加しましょう

// In src/Controller/AppController.php
namespace App\Controller;
 
use Cake\Controller\Controller;
 
class AppController extends Controller
{
    public function initialize()
    {
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password'
                    ]
                ]
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login'
            ]
        ]);
        
        // Allow the display action so our pages controller
        // continues to work.
        $this->Auth->allow(['display']);
    }
}

ここではflashとauthコンポーネントをロードしています。

usernameにemail,passwordにpasswordを使用します。このへんはCakePHP2と同様ですね。

現在ログインしていないので、どのページにアクセスしても/users/loginにリダイレクトされます。

では/users/loginページを実装しましょう。

// In src/Controller/UsersController.php

public function login()
{
    if ($this->request->is('post')) {
        $user = $this->Auth->identify();
        if ($user) {
            $this->Auth->setUser($user);
            return $this->redirect($this->Auth->redirectUrl());
        }
        $this->Flash->error('Your username or password is incorrect.');
    }
}
// src/Template/Users/login.ctp
<h1>Login</h1>
<?= $this->Form->create() ?>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>

これでログイン機能ができました。 f:id:yuzurus:20150609230203p:plain

ログアウト機能の実装

UsersController.phpにlogoutメソッドを追加します。

public function logout()
{
    $this->Flash->success('You are now logged out.');
    return $this->redirect($this->Auth->logout());
}

ここもCakePHP2と同様ですね。

ユーザ登録の有効化

現在ユーザ登録しようとしてもログイン画面へリダイレクトされるので、それをさせないようにします。

// In src/Controller/UsersController.php
public function beforeFilter(\Cake\Event\Event $event)
{
    $this->Auth->allow(['add']);
}

addメソッドが認証を必要としないことを示します。

ブックマークへのアクセス制限

現在ログインしさえすればすべてのユーザのブックマークにアクセスできてしまします。

ですので、その人が登録したブックマークのみアクセス出来るようにしましょう。

authorizationアダプターを使用します。

// In src/Controller/AppController.php
public function initialize()
{
    $this->loadComponent('Flash');
    $this->loadComponent('Auth', [
        'authorize'=> 'Controller',//added this line
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'unauthorizedRedirect' => $this->referer()
    ]);
 
    // Allow the display action so our pages controller
    // continues to work.
    $this->Auth->allow(['display']);
}

// add
public function isAuthorized($user)
{
    return false;
}

基本はアクセス禁止で、ホワイトリスト方式が良いでしょう。

// In src/Controller/BookmarksController.php
public function isAuthorized($user)
{
    $action = $this->request->params['action'];
 
    // The add and index actions are always allowed.
    if (in_array($action, ['index', 'add', 'tags'])) {
        return true;
    }
    // All other actions require an id.
    if (empty($this->request->params['pass'][0])) {
        return false;
    }
 
    // Check that the bookmark belongs to the current user.
    $id = $this->request->params['pass'][0];
    $bookmark = $this->Bookmarks->get($id);
    if ($bookmark->user_id == $user['id']) {
        return true;
    }
    return parent::isAuthorized($user);
}

これで表示、編集、削除しても元のページに戻ります。 エラーページも用意しましょう

// In src/Template/Layout/default.ctp
// Under the existing flash message.
<?= $this->Flash->render('auth') ?>

一覧ページとフォームの修正

追加機能と一覧ページには以下の問題があります。 1. ブックマークを追加や編集する場合、ユーザを選択出来てしまう 2. 一覧ページでは他のユーザのブックマークまで閲覧できてしまう

これらを修正していきます。

フォームの修正

Bookmarks/add.ctpから input('user_id') を削除してユーザIDを編集できないようにしましょう。

またBookmarksController.phpをaddメソッドとeditメソッドを以下のように修正しましょう。

public function add()
{
    $bookmark = $this->Bookmarks->newEntity();
    if ($this->request->is('post')) {
        $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->data);
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
}
public function edit($id = null)
{
    $bookmark = $this->Bookmarks->get($id, [
        'contain' => ['Tags']
    ]);
    if ($this->request->is(['patch', 'post', 'put'])) {
        $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->data);
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
}

一覧画面の修正

続いて一覧画面でログインしているユーザーのブックマークだけを表示するようにします。 pagenate()に条件を加えて以下の様に修正してください。

public function index()
{
    $this->paginate = [
        'conditions' => [
            'Bookmarks.user_id' => $this->Auth->user('id'),
        ]
    ];
    $this->set('bookmarks', $this->paginate($this->Bookmarks));
}

タグ機能の修正

現状ではTagsControllerがすべてのアクセスを拒否するため、新しいタグの登録ができません。 アクセスを許可する代わりに、区切りのテキストで入力するUIを開発しましょう。 これにより良いユーザユーザーエクスペリエンスを手に入れられるだけでなく、ORMのさらなる機能を使ってみることができます。

処理済フィールドの追加

エンティティへのアクセスをシンプルな方法にするために、エンティティに仮想的で処理済なフィールドを追加しましょう。src/Model/Entity/Bookmark.phpに以下を追加してください。

use Cake\Collection\Collection;

protected function _getTagString()
{
    if (isset($this->_properties['tag_string'])) {
        return $this->_properties['tag_string'];
    }
    if (empty($this->tags)) {
        return '';
    }
    $tags = new Collection($this->tags);
    $str = $tags->reduce(function ($string, $tag) {
        return $string . $tag->title . ', ';
    }, '');
    return trim($str, ', ');
}

これは私たちが$bookmark->tag_stringとすれば処理済のプロパティにアクセスすることを可能にします。 後で、このプロパティをフォームのinputで使います。 また保存に必要なのでtag_stringプロパティをエンティティの_accessibleリストに追加してください。

    protected $_accessible = [
        'user_id' => true,
        'title' => true,
        'description' => true,
        'url' => true,
        'user' => true,
        'tags' => true,
        'tag_string' => true,
    ];

ビューの更新

エンティティが更新されたので、新しいinput要素を追加することができます。add、editの、既存のtags._idsinputを以下のように変更してください。

<?= $this->Form->input('tag_string', ['type' => 'text']) ?>

タグ文字列の永続化

既存のタグを文字列として表示できるようになりました。同様にタグ文字列を保存できるようにしてみましょう。

tag_stringを$_accessibleに追加したので、ORMはこのデータをリクエストからエンティティにコピーします。

beforeSave()フックメソッドを使用して、エンティティに対してタグ文字列のパースとタグリストの検索/追加ができます。以下のコードをBookmarksTable.phpに追加してください。

public function beforeSave($event, $entity, $options)
{
    if ($entity->tag_string) {
        $entity->tags = $this->_buildTags($entity->tag_string);
    }
}

protected function _buildTags($tagString)
{
    $new = array_unique(array_map('trim', explode(',', $tagString)));
    $out = [];
    $query = $this->Tags->find()
        ->where(['Tags.title IN' => $new]);

    // Remove existing tags from the list of new tags.
    foreach ($query->extract('title') as $existing) {
        $index = array_search($existing, $new);
        if ($index !== false) {
            unset($new[$index]);
        }
    }
    // Add existing tags.
    foreach ($query as $tag) {
        $out[] = $tag;
    }
    // Add new tags.
    foreach ($new as $tag) {
        $out[] = $this->Tags->newEntity(['title' => $tag]);
    }
    return $out;
}

まとめ

ブックマークアプリケーションにおいて、簡単なログイン認証、アクセスコントロールなどを実装できました。 これを機会にCakePHP3に触れる人が増えてくればなによりです。

【チュートリアル】最新フレームワークCakePHP3でブックマーカーを作ってみる その1

そろそろちゃんと最新フレームワークCakePHP3を触らないといけない気がしてきたので、チュートリアルとしてCakePHP3を使ってブックマーカーを作ってみます。

環境はMacOSXVirtualbox+VagrantCentOS乗っけてます。

インストール

PHPのインストール

PHPをインストールしていない場合はまずPHPから

PHP5.6系をインストールします。

リポジトリはremiで。

% rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
% yum install --enablerepo=remi --enablerepo=remi-php56 php php-intl php-mbstring php-mysqlnd
$ php -v
PHP 5.6.9 (cli) (built: May 15 2015 09:40:22)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies
    with Xdebug v2.3.2, Copyright (c) 2002-2015, by Derick Rethans

Composerのインストール

よもやぺちぱーでComposerを使っていない人はいないと思いますが、一応インストール方法を。

グローバルにインストールしちゃいます。

$ curl -sS https://getcomposer.org/installer | php
% mv composer.phar /usr/local/bin/composer

Mysqlのインストール

とりあえず5.1以上をインストールしてください。

インストール方法は面倒なので飛ばします。

今回は5.6.24を使います。

CakePHP3のインストール

さていよいよCakePHP3のインストールです。

ワンライナーでインストールできてしまいます。良い時代ですね。

今回はプロジェクト名をbookmarkerで。

composer create-project --prefer-dist cakephp/app bookmarker
Installing cakephp/app (3.0.3)
  - Installing cakephp/app (3.0.3)
    Downloading: 100%

Created project in bookmarker
Loading composer repositories with package information
Installing dependencies (including require-dev)
省略
  - Installing cakephp/cakephp (3.0.6)
    Loading from cache
  - Installing cakephp/migrations (1.1.1)
    Downloading: 100%
  - Installing cakephp/debug_kit (3.1.5)
    Loading from cache
  - Installing cakephp/bake (1.0.9)
    Downloading: 100%
省略
Created `config/app.php` file
Set Folder Permissions ? (Default to Y) [Y,n]? Y
省略
Updated Security.salt value in config/app.php

MigrationsがCakeDCじゃなくなったのですね。

パーミッション云々聞かれるのでYとしましょう。

Saltも勝手にランダム値に設定してくれるみたいです。

2系でもインストールは楽だったのですが更に楽になりましたね。

ディレクトリ構成を見てみましょう。

$ tree -L 2
.
|-- README.md
|-- bin
|   |-- cake
|   |-- cake.bat
|   `-- cake.php
|-- composer.json
|-- composer.lock
|-- config
|   |-- app.default.php
|   |-- app.php
|   |-- bootstrap.php
|   |-- bootstrap_cli.php
|   |-- paths.php
|   |-- routes.php
|   `-- schema
|-- index.php
|-- logs
|   |-- cli-error.log
|   `-- empty
|-- phpunit.xml.dist
|-- plugins
|   `-- empty
|-- src
|   |-- Console
|   |-- Controller
|   |-- Model
|   |-- Shell
|   |-- Template
|   `-- View
|-- tests
|   |-- Fixture
|   |-- TestCase
|   `-- bootstrap.php
|-- tmp
|   |-- Bake-Controller-controller-ctp.php
|   |-- Bake-Element-Controller-add-ctp.php
|   |-- Bake-Element-Controller-delete-ctp.php
|   |-- Bake-Element-Controller-edit-ctp.php
|   |-- Bake-Element-Controller-index-ctp.php
|   |-- Bake-Element-Controller-view-ctp.php
|   |-- Bake-Element-form-ctp.php
|   |-- Bake-Layout-default-ctp.php
|   |-- Bake-Model-entity-ctp.php
|   |-- Bake-Model-table-ctp.php
|   |-- Bake-tests-fixture-ctp.php
|   |-- Bake-tests-test-case-ctp.php
|   |-- add-ctp.php
|   |-- cache
|   |-- debug_kit.sqlite
|   |-- edit-ctp.php
|   |-- index-ctp.php
|   |-- sessions
|   |-- tests
|   `-- view-ctp.php
|-- vendor
|   |-- aura
|   |-- autoload.php
|   |-- bin
|   |-- cakephp
|   |-- cakephp-plugins.php
|   |-- composer
|   |-- dnoegel
|   |-- empty
|   |-- ircmaxell
|   |-- jakub-onderka
|   |-- mobiledetect
|   |-- nesbot
|   |-- nikic
|   |-- psr
|   |-- psy
|   |-- robmorgan
|   `-- symfony
`-- webroot
    |-- css
    |-- favicon.ico
    |-- img
    |-- index.php
    `-- js

38 directories, 40 files

srcディレクトリにMVCが入るようになったのですね。

起動してみる

ビルドインサーバがあるので試してみます。

ローカルホスト上で動いている場合は

$ bin/cake server
Welcome to CakePHP v3.0.6 Console
---------------------------------------------------------------
App : src
Path: /vagrant/blog/bookmarker/src/
DocumentRoot: /vagrant/blog/bookmarker/webroot
---------------------------------------------------------------
built-in server is running in http://localhost:8765/
You can exit with `CTRL-C`

http://localhost:8765/ でアクセスできます。

仮想マシン上で動いている場合はホストを指定しなければなりません。

bin/cake server -H 0.0.0.0

Welcome to CakePHP v3.0.6 Console
---------------------------------------------------------------
App : src
Path: /vagrant/blog/bookmarker/src/
DocumentRoot: /vagrant/blog/bookmarker/webroot
---------------------------------------------------------------
built-in server is running in http://0.0.0.0:8765/
You can exit with `CTRL-C`

vagrantでIPを192.168.33.50と指定しているので

http://192.168.33.50:8765/ でアクセスできます。

f:id:yuzurus:20150609113143p:plain

起動できました。

データベース(MySQL)の設定

CakePHPからMySQLへアクセスできるように設定をします。

Config/app.phpを自分の環境に合わせて設定してください。

    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            /**
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => '',
            'database' => 'cake_bookmarks',
            'encoding' => 'utf8',
            'timezone' => 'Asia/Tokyo',
            'cacheMetadata' => true,

            /**
             * Set identifier quoting to true if you are using reserved words or
             * special characters in your table or column names. Enabling this
             * setting will result in queries built using the Query Builder having
             * identifiers quoted when creating SQL. It should be noted that this
             * decreases performance because each query needs to be traversed and
             * manipulated before being executed.
             */
            'quoteIdentifiers' => false,

            /**
             * During development, if using MySQL < 5.6, uncommenting the
             * following line could boost the speed at which schema metadata is
             * fetched from the database. It can also be set directly with the
             * mysql configuration directive 'innodb_stats_on_metadata = 0'
             * which is the recommended value in production environments
             */
            //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
        ],

timezoneをAsia/Tokyoにした場合以下のようなエラーが出るかもしれません。

Exception: SQLSTATE[HY000]: General error: 1298 Unknown or incorrect time zone: 'Asia/Tokyo' in [/vagrant/bookmarker/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php, line 94]
php-intl

MySQLタイムゾーンの設定に不備があるので下記のコマンドを実行してください

$ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql -p

bakeを使ってコードを生成する

CakePHPチュートリアルでお馴染みのbakeでコードを生成していきます。

$ bin/cake bake all users
$ bin/cake bake all bookmarks
$ bin/cake bake all tags

コントローラ、モデル、ビュー、テストケースが生成されます。

テスト大事ですね!

では http://192.168.33.50:8765/bookmarks にアクセスしてみましょう。 f:id:yuzurus:20150609165505p:plain

ユーザを登録したりブックマークを登録したりタグを登録してみましょう。

パスワードのハッシュ化

f:id:yuzurus:20150609223405p:plain

ユーザ登録するとパスワードがプレーンテキストで保存されています。

例えばこの状態でサービスリリースをしてSQLインジェクションなどでユーザ情報が流出すると、このIDとパスワードを使って他のサービスのログインを試み、2次被害してしまいます。そういうことを防ぐためにパスワードは必ず暗号化して保存しなければなりません。

CakePHP2ではModelはModelクラスひとつでしたが、CakePHP3ではTableとEntityに分かれています。 TableはDBのテーブルにアクセスするクラスで、Entityは単一のレコードを扱うクラスです。

パスワードのハッシュ化ロジックはUserのEntityクラスのpasswordセッターに追加します。

_set{カラム名}とすると、カラムの保存前にメソッドが呼び出されるようです。

<?php
namespace App\Model\Entity;
use Cake\Auth\DefaultPasswordHasher;
use Cake\ORM\Entity;

/**
 * User Entity.
 */
class User extends Entity
{

    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * @var array
     */
    protected $_accessible = [
        'email' => true,
        'password' => true,
        'bookmarks' => true,
    ];


    // Code from bake.

    protected function _setPassword($value)
    {
        $hasher = new DefaultPasswordHasher();
        return $hasher->hash($value);
    }

}

DefaultPasswordHasherはBCrypt(Blowfish暗号)になります。

これで、パスワードはハッシュ化されて保存されるようになります。

CakePHP2系ではSecurityコンポーネントで指定できるソルトは$2a$のみでしたが、DefaultPasswordHasherではデフォルトで$2y$なのですね。

特定タグのブックマークを取得する

http://192.168.33.50:8765/bookmarks/tagged/funny/cat/gifs でアクセスした時にfunny, cat, gifsのタグが付いたブックマークをを表示する、という機能をつけてみます。

routeの実装

config/routes.phpに以下を追加

Router::scope(
    '/bookmarks',
    ['controller' => 'Bookmarks'],
    function ($routes) {
        $routes->connect('/tagged/*', ['action' => 'tags']);
    }
);

CakePHP2を経験してる人であれば意味はわかりますね。

では対応するメソッドを実装します。

tagsメソッドの実装

public function tags()
{
    $tags = $this->request->params['pass'];
    $bookmarks = $this->Bookmarks->find('tagged', [
        'tags' => $tags
    ]);
    $this->set(compact('bookmarks', 'tags'));
}

次に

$this->Bookmarks->find('tagged', [
        'tags' => $tags
]);

に当たる部分を実装します。

finder メソッドの実装

BookmarksTable.phpに以下を実装します。

public function findTagged(Query $query, array $options)
{
    $fields = [
        'Bookmarks.id',
        'Bookmarks.title',
        'Bookmarks.url',
    ];
    return $this->find()
        ->distinct($fields)
        ->matching('Tags', function ($q) use ($options) {
            return $q->where(['Tags.title IN' => $options['tags']]);
        });
}

これがカスタムファインダーメソッドです。これはCakePHP3の目玉機能の一つでクエリを再利用可能にします。

Viewの実装

ロジックができたので次はそれを表示するViewを実装します。

Bookmarks/tags.ctpに実装します。

<h1>
    Bookmarks tagged with
    <?= $this->Text->toList($tags) ?>
</h1>

<section>
<?php foreach ($bookmarks as $bookmark): ?>
    <article>
        <h4><?= $this->Html->link($bookmark->title, $bookmark->url) ?></h4>
        <small><?= h($bookmark->url) ?></small>
        <?= $this->Text->autoParagraph($bookmark->description) ?>
    </article>
<?php endforeach; ?>
</section>

これで表示できるようになります。 「探偵物」というタグに「金田一少年の事件簿」というブックマークを紐付けたので以下のような表示になります。

http://192.168.33.50:8765/bookmarks/tagged/探偵物 f:id:yuzurus:20150609222758p:plain

とりあえず今日はここまで。 次回はログイン、ログアウト、アクセス制限などの機能を追加していきます。

【チュートリアル】最新フレームワークCakePHP3でブックマーカーを作ってみる その2

参考 http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html

CakePHPで学ぶ継続的インテグレーション (impress top gear)

CakePHPで学ぶ継続的インテグレーション (impress top gear)

【MeCab】マルコフ連鎖テキストを吐き出すPHPライブラリを作った。

yuzurus.hatenablog.jp

前回MeCabのインストール方法をご紹介しました。 そのMeCabを使ったマルコフ連鎖テキストを吐き出すPHPライブラリを作成しました。 Packagistにも公開したので簡単にインストール出来るようになっています。

インストール

  1. MeCabのインストール
  2. ComposerをインストールしていなかったらComposerをインストール
$ composer require 'yuzuru-s/markovchain:dev-master'

これで準備は完了です。

使い方

require_once 'vendor/autoload.php';
$mc = new YuzuruS\Markovchain\Markovchain();

$text = '繁栄を築き上げた人類は、突如出現した“天敵”「巨人」により滅亡の淵に立たされた。生き残った人類は、「ウォール・マリア」、「ウォール・ローゼ」、「ウォール・シーナ」という巨大な三重の城壁の内側に生活圏を確保することで、辛うじてその命脈を保っていた。城壁による平和を得てから約100年後。いつしか人類は巨人の脅威を忘れ、平和な日々の生活に埋没していた。';

$markovText = $mc->makeMarkovText($text);

echo $text."\n";
echo '↓'."\n";
echo $markovText."\n";

結果

繁栄を築き上げた人類は、突如出現した“天敵”「巨人」により滅亡の淵に立たされた。生き残った人類は、「ウォール・マリア」、「ウォール・ローゼ」、「ウォール・シーナ」という巨大な三重の城壁の内側に生活圏を確保することで、辛うじてその命脈を保っていた。城壁による平和を得てから約100年後。いつしか人類は巨人の脅威を忘れ、平和な日々の生活に埋没していた。

繁栄を築き上げた。いつしか人類は、突如出現した。生き残った人類は、辛うじてその命脈を得ていた“天敵”「ウォール・シーナ」、平和な日々の城壁による平和を忘れ、「ウォール・マリア」、「ウォール・シーナ」、辛うじてその命脈を確保することで、突如出現した“天敵”「ウォール・シーナ」、「巨人の脅威を保ってから約100年後。城壁による平和な三重の城壁による平和を忘れ、辛うじてその命脈を保ってから約100年後。

まとめ

マルコフ連鎖は所謂「人工無能」に使われているようですね。

とりあえず自分で使うために適当に作ったのでまだ汎用的に作ってないです。 時間があったらもう少し汎用的になるように修正します。

格安SIM(MVNO)とRaspberry Pi(ラズベリーパイ)でモバイルルータを作った 〜初期設定から完成まで〜

f:id:yuzurus:20150523195517j:plain
最近ジワジワ人気になってきた格安SIM(MVNO)とRaspberry Pi(ラズベリーパイ)。 今回はこの2つを使ってモバイルルータを作ってみたいと思います。

格安SIM(MVNO)とは?

格安SIM(MVNO)は非常に低価格でモバイル通信できます。 安いものだと月額500円以下のプランもあります。 非常に低価格なのですが、格安SIM(MVNO)を使うためには殆どの場合ドコモ端末かSIMフリー端末が必要となります。

Raspberry Pi(ラズベリーパイ)とは?

Raspberry Pi(ラズベリーパイ)とは手のひらサイズの超小型コンピュータで電子工作やプログラミング等に使えます。 価格も非常に安く、5000円ほどで手に入ります。

用意するもの

・Raspberry Pi Model B+(4266円)

Raspberry Pi Model B+ (Plus)

Raspberry Pi Model B+ (Plus)

必須です。最近Amazonでも取り扱うようになりましたね。

・L-02C (4500円)

ヤフオクで中古品(2000円ぐらい)が出回っているのでそちらでも可。

・DMM mobile (最安プラン 660円)
Alt DMM mobile
今回は手持ちの格安SIM(MVNO)のDMM mobileを使用しました。

無線LANアダプタ (713円)

BUFFALO 無線LAN子機 コンパクトモデル 11n技術・11g/b対応 WLI-UC-GNM

BUFFALO 無線LAN子機 コンパクトモデル 11n技術・11g/b対応 WLI-UC-GNM

安くて良いです。

・8GB microSDカード (824円)

OSを入れるのに使用します。
以前のRaspberry Pi(ラズベリーパイ)はSDカードでしたが最新のモデルはmicroSDカードなのでご注意を。

・軽いモバイルバッテリー

今回は私の手持ちのモバイルバッテリーで代用しましたが持ち運びを考えると軽いものが良いです。

・有線LANケーブル (583円)

セットアップに使います。

RaspberryPi(ラズベリーパイ)の初期設定

作業PCはMacを使用します。
まずmicroSDカードにOSイメージを焼きます。OSはせっかくなのでRaspbian(ラズビアン)を使います。
公式サイトからRASPBIANを選択しダウンロード、解凍します。今回は2015-05-05リリースのものを使用します。ダウンロードが終わったらSDカードをMacに接続します。

df -hでSDカードのディスク名をチェック

$ df -h                                                               
Filesystem                                                    Size   Used  Avail Capacity  iused     ifree %iused  Mounted on
/dev/disk2s1                                                  29Gi  2.3Mi   29Gi     1%        0         0  100%   /Volumes/NO NAME

ディスク名は/dev/disk2s1でした。

diskutilでアンマウント

# diskutil unmountDisk /dev/disk2s1                                         
Password:
Unmount of all volumes on disk2 was successful

成功です。

ddコマンドでSDカードにOSイメージを書き込み

# dd if=2015-05-05-raspbian-wheezy.img of=/dev/disk2s1 bs=4m                  
781+1 records in
781+1 records out
3276800000 bytes transferred in 290.594687 secs (11276187 bytes/sec)

30分ほど時間がかかりました。これでSDカードにOSイメージが書き込まれました。

RaspberryPi(ラズベリーパイ)へ接続

f:id:yuzurus:20150523195645j:plain SDカード、有線LANをRaspberryPi(ラズベリーパイ)へ接続したあと、microUSBポートに電源を差し込みます。
赤と緑のLEDが点灯し緑が点滅すれば成功です。
私は初めて接続した時、赤と緑のLEDが点灯したまま点滅しませんでした。焼きがうまく行っていなかったようで、再度焼き直したら起動できました。
使用しているルータにアクセスしてRaspberryPi(ラズベリーパイ)に割り振られているIPアドレスを確認。今回は192.168.11.13でした。

MacからSSHを使って接続してみます。usernameはpi, passwordはraspberryです。

$ ssh pi@192.168.11.13           
The authenticity of host '192.168.11.13 (192.168.11.13)' can't be established.
RSA key fingerprint is 0d:6e:e9:9d:25:28:2b:51:59:c0:34:0e:6b:78:01:82.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.11.13' (RSA) to the list of known hosts.
pi@192.168.11.13's password:
Linux raspberrypi 3.18.11+ #781 PREEMPT Tue Apr 21 18:02:18 BST 2015 armv6l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri May 22 05:04:12 2015

無事接続できました。

初期設定

今後はRaspberry Pi(ラズベリーパイ)上での作業となります。

# raspi-config

上記のコマンドを実行すると下記のような画面が表示されます。
f:id:yuzurus:20150523195643p:plain

ここで日本locale設定だけにします。

  1. Expand Filesystemを選択します。
  2. Internationalisation Options > Change Locale > ja_JP.UTF-8で日本語環境にします。
  3. Internationalisation Options > Change Timezone > Asia > Tokyoで日本時間にします。

パッケージ更新

パッケージ更新
# apt-get update
# apt-get upgrade

Vimをインストール

ついでにみんな大好きVimをインストールしておきましょう。

# apt-get install vim
# update-alternatives --set editor /usr/bin/vim.tiny

これでRaspberry Pi(ラズベリーパイ)の初期設定はひとまず完了です。

WiFiアクセスポイント機能有効化

Raspberry Pi(ラズベリーパイ)に無線LANアクセスポイントであるWLI-UC-GNMを差し込んでください。 Wi-Fiアクセスポイント機能を有効にするためにhostapdを導入します。
有線LANのeth0にDHCPIPアドレスを割り当て、無線LANのwlan0に固定IPアドレスを割り当てます。

# vim /etc/network/interfaces

auto lo
 
iface lo inet loopback
iface eth0 inet dhcp
 
allow-hotplug wlan0
auto wlan0
iface wlan0 inet static
address 192.168.100.1
network 192.168.100.0
netmask 255.255.255.0
gateway 192.168.11.1

次にhostapdをインストールします。

# apt-get install hostapd

ここでhostapdの設定を行いますが、使用するアダプタ(ドライバ)によって手順が変わってきます。
今回は「用意するもの」で挙げたWLI-UC-GNMを使った設定を行います。
lsmodして、ドライバを確認します。

# /sbin/lsmod

ドライバがmac80211ベースでした。 こちらは本家のhostapdがサポートしているのですが、apt-getでインストールされるものはバージョンが古いためhostapdを入れ替える必要があります。 公式サイトから最新のソースをダウンロード、コンパイルします。

$ cd /tmp
$ wget http://w1.fi/releases/hostapd-2.0.tar.gz
$ tar xvf hostapd-2.0.tar.gz

コンパイルに必要なライブラリをインストールします。

# apt-get install libnl-genl-3-dev libssl-dev

必要な設定を加えてコンパイルします。

$ cd hostapd-2.0/hostapd
$ cp defconfig .config
$ echo "CONFIG_LIBNL32=y" >> .config
$ make
# make install

hostapdの設定ファイルを修正。 ssid, channel, wpa_passphraseは適宜変更してください。

interface=wlan0
driver=nl80211
ssid=korokkeyasan
hw_mode=g
channel=6
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=korokkeyasan
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

hostapd起動のための設定をします。
DAEMON_CONFがコメントアウトされているので下記のように修正してください。

# vim /etc/default/hostapd
DAEMON_CONF="/etc/hostapd/hostapd.conf"

WPASupplicantを無効にしておきます。一応適当なところにバックアップしておきます。

# mv /usr/share/dbus-1/system-services/fi.epitest.hostap.WPASupplicant.service /tmp

DHCPサーバの導入

IPアドレスを自動で割り振るDHCPサーバを導入していきます。

# apt-get install isc-dhcp-server

設定ファイルを編集します。(設定した部分だけ抜粋)

# vim /etc/dhcp/dhcpd.conf

#コメントアウト
#option domain-name "example.org";
#option domain-name-servers ns1.example.org, ns2.example.org;

#アンコメント
# 正当な DHCP サーバーであることの宣言 
authoritative;

# ネットワークアドレスとサブネットマスク指定 
subnet 192.168.100.0 netmask 255.255.255.0 {

  # 貸し出すIPアドレスの範囲指定
  range 192.168.100.10 192.168.100.50;

  # ブロードキャストアドレス指定
  option broadcast-address 192.168.100.255;
  
  # デフォルト貸出期間
  default-lease-time 600;

  # 最大貸出期間
  max-lease-time 7200;

  # ゲートウェイアドレス指定
  option routers 192.168.100.1;

  # ドメイン名指定
  option domain-name "local";

  # ネームサーバーのホスト名 (Google)
  option domain-name-servers 8.8.8.8, 8.8.4.4;
}

DHCPサーバの起動設定をします。

# vim /etc/default/isc-dhcp-server
INTERFACES="wlan0"

先ほど設定したhostapdをデーモンとして起動するようにしたいので下記のように設定します。

# service hostapd start

また起動時にhostapdを起動させたいので下記のように設定します。

# update-rc.d hostapd enable 

DHCPサーバを起動します。

# service isc-dhcp-server start

また起動時にDHCPサーバを起動させたいので下記のように設定します。

#  update-rc.d isc-dhcp-server enable

これで自動でIPアドレスが割り振られるようになりました。

IPマスカレードの設定

カーネルの機能なので、ここでは特に何かインストールする必要はありません。

# vim /etc/sysctl.conf
 
# アンコメント
net.ipv4.ip_forward=1

一度再起動してこの設定を有効にします。

# reboot

再起動が終わったら再度SSH接続してください。 IPテーブルの設定でIPマスカレードを有効にします。

# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

これで有線LAN接続状態でクライアントが無線でインターネットに接続出来るようになりました!

LTEネットワーク接続

f:id:yuzurus:20150523195644j:plain いよいよ有線LANで接続ではなくLTEネットワークでの接続設定をしていきます。 予めL-02CをRaspberry Pi(ラズベリーパイ)に挿入しておいてください。

# apt-get install wvdial
# apt-get install eject

wvdialはダイアルアップ接続用ソフトなのですがejectはCD/DVD-ROMドライブをEjectするためのコマンド。 なぜか今回使用するL-02CはRaspberry Pi(ラズベリーパイ)につなぐとCD-ROMドライブとして認識され、Ejectしないと使えないという謎仕様。だから安いんですけどね。。

# eject sr0

確認します。

$ lsusb

L-02Cが一覧に出てくればOKです。
次に使用するSIMの設定を行っていきます。今回はDMM mobile(IIJmio系)を想定しています。

# vim /etc/wvdial.conf

[Dialer Defaults]
Init1 = ATZ
Init2 = AT+CGDCONT=1,"IP","iijmio.jp"
Init3 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Dial Attempts = 3
Stupid Mode = 1
Modem Type = Analog Modem
Dial Command = ATD
Stupid Mode = yes
Baud = 460800
New PPPD = yes
APN = iijmio.jp
Modem = /dev/ttyUSB2
ISDN = 0
Phone = *99***1#
Password = iij
Username = mio@iij
Carrier Check = no

Phoneは"99*1#"でも"99#"でも可能です。 いよいよ接続です。

# wvdial

PPP接続できました!! IPマスカレードの設定をします。

# iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

起動時にPPP接続できるようにします。

# vim /etc/init.d/wvdialloop.sh
 
#!/bin/bash
LOG='/var/log/wvdial'
eject /dev/sr0
(
  while : ; do
  wvdial 2>&1
  sleep 15
  done
)   2>&1 > ${LOG} < /dev/null &
# chmod +x /etc/init.d/wvdialloop.sh
# update-rc.d wvdialloop.sh defaults

rc.localにIPマスカレードの設定を追加します。

# vim /etc/rc.local
 
# exit 0 の前に追加
iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

これで電源を入れたらLTEネットワークに接続されモバイルルータとして機能するようになりました!!

その他

初期設定のままSSH接続していましたが、ちゃんと運用するのであればパスワードを変更するなり鍵を設定するなりしてセキュリティを高めてください。
また結構熱を持つのでRaspberry Pi(ラズベリーパイ)のケースを買ったほうが良いかもしれません。

まとめ

モバイルネットワークにつながると夢が広がりますね。とはいっても実はドコモ系MVNOはグローバルIPアドレスが割り振られないようで、遠隔操作やWEBサーバとして使うのは難しいようです。。ngrokを使うといけそうな気もするので次回挑戦してみたいと思います。

参考にしたサイト

http://qiita.com/makoto_kw/items/393e098f214f81449c9f
http://sideb.hatenablog.com/entry/mvno_kakuyasu_sim_raspberry_pi_lte_3g_mobile_router

マルコフ連鎖の実験のためにMeCabをCentOSにインストール

マルコフ連鎖の実験をしたいがために、 京都大学情報学研究科と日本電信電話株式会社コミュニケーション科学基礎研究所が開発しているオープンソース形態素解析エンジン「MeCab」 (和布蕪 めかぶ)をインストールするためのメモです。

本体をインストール

$ cd /tmp
$ wget https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7cENtOXlicTFaRUE
$ tar zxfv mecab-X.X.tar.gz
$ cd mecab-X.X
$ ./configure --enable-utf8-only
$ make
$ make check
# make install

辞書ファイルをインストール

$ cd /tmp
$ wget https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM
$ tar zxfv mecab-ipadic-2.7.0-XXXX.tar.gz
$ cd mecab-ipadic-2.7.0-XXXX
$ ./configure --with-charset=utf8
$ make
# make install

試してみる

$ mecab
すもももももももものうち
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

うまくいってるっぽい。 このライブラリを使って今後マルコフ連鎖の実験をしていく。

入門 自然言語処理

入門 自然言語処理

jQueryで$(form).submit()でフォームが送信されない場合の対処法

jQueryで、$(form).submit()などを使っても下記のようなボタンにするとフォームが送信されない場合があります。

<input type="submit" id="submit"  value="post" />
<input type="submit" class="submit"  value="post" />
<input type="submit" name="submit"  value="post" />

submitボタンのid,class,name属性に「submit」という名前を使用すると送信できないので、使わないようにしましょう。