コントローラーやモデルを作る上での注意
クラス名は被らないようにすること(後々困ったりする ネームスペースを使用できるようにすれば多少はましになるかも)
フォルダで分けた場合はプレフィックスをつける等
loadまでは問題なかったがメソッドを呼び出したところでメモリ不足になった。
同じ名前のクラスは作らない
プロファイラ出力
$this->output->enable_profiler(TRUE);
JSON出力
output後も処理は続くので注意
- コントローラから使うとき
$this->output
->set_content_type('application/json')
->set_status_header(200)
->set_output(json_encode([]));
return;
- コントローラから使うときまたは、コントローラから呼び出されるメソッドで使うとき
$this->output
->set_content_type('application/json')
->set_status_header(200)
->set_output(json_encode([]))
->_display();
exit(0);
APIの時はJSONでCSRFのエラーを出すようにする
APIの時はJSONでCSRFのエラーを出すようにExceptionsを拡張しようとしたところ$CI
がnull
になってしまう。
ということで以下は動きません。
defined('BASEPATH') OR exit('No direct script access allowed');
require_once BASEPATH.'core/Controller.php';
class MY_Exceptions extends CI_Exceptions {
protected $CI;
public function __construct()
{
$this->CI =& get_instance();
parent::__construct();
}
public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
{
if ($this->CI->uri->segment(1) === 'api')
{
// APIの場合JSONで返す
$this->CI->output
->set_content_type('application/json')
->set_status_header($status_code)
->set_output(json_encode([
'message' => $message,
'csrf_token_name' => $this->CI->security->get_csrf_token_name(),
'csrf_hash' => $this->CI->security->get_csrf_hash()
]))->_display();
exit(0);
}
// 通常
return parent::show_error($heading, $message, $template, $status_code);
}
public function show_exception($exception)
{
if ($this->CI->uri->segment(1) === 'api')
{
// APIの場合JSONで返す
$this->CI->output
->set_content_type('application/json')
->set_status_header(500)
->set_output(json_encode([
'message' => $this->CI->exception->getMessage(),
'csrf_token_name' => $this->CI->security->get_csrf_token_name(),
'csrf_hash' => $this->CI->security->get_csrf_hash()
]))->_display();
exit(0);
}
// 通常
return parent::show_exception($exception);
}
}
とりあえず個別でnew
してもここについては問題なさそうなのでしてみる。
ここでの注意はCI_Security
はnew
するだけではhash
が更新されるだけでクッキーには登録されないのでcsrf_set_cookie
も行う必要がある。それを行わないと判定はクッキーのものとinput
を比較しているので永遠に一致しない。
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Exceptions extends CI_Exceptions {
public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
{
$uri = new CI_URI();
if ($uri->segment(1) === 'api')
{
// APIの場合JSONで返す
$output = new CI_Output();
$security = new CI_Security();
$security->csrf_set_cookie();
$output
->set_content_type('application/json')
->set_status_header($status_code)
->set_output(json_encode([
'message' => $message,
'csrf_token_name' => $security->get_csrf_token_name(),
'csrf_hash' => $security->get_csrf_hash()
]))->_display();
exit(0);
}
// 通常
return parent::show_error($heading, $message, $template, $status_code);
}
public function show_exception($exception)
{
$uri = new CI_URI();
if ($uri->segment(1) === 'api')
{
// APIの場合JSONで返す
$output = new CI_Output();
$security = new CI_Security();
$security->csrf_set_cookie();
$output
->set_content_type('application/json')
->set_status_header(500)
->set_output(json_encode([
'message' => $exception->getMessage(),
'csrf_token_name' => $security->get_csrf_token_name(),
'csrf_hash' => $security->get_csrf_hash()
]))->_display();
exit(0);
}
// 通常
return parent::show_exception($exception);
}
}
API判定はリクエストヘッダに独自ヘッダ(X-から始まる)を付けたりして判定するのでも良いかも
X-Custom-Header: api
とか
$_SERVER['HTTP_X_CUSTOM_HEADER']
で判定(CI_Input
をnewすると応答が返ってこなくなったのでinputは使えない)(CIが使える箇所では`$this->input->get_request_header('X-Custom-Header')で)
config値の取得
$this->config->item('base_url');
リクエストメソッド
リクエストパラメータ
http://codeigniter.jp/user_guide/3/libraries/input.html#CI_Input::post
http://codeigniter.jp/user_guide/3/libraries/input.html#CI_Input::get
アイテムが存在しない場合NULLを返す
XSS
入力を画面に出力する際は
xss_cleanではscript
タグは[removed]
に置き換えたり有害と思われるものを削除したりHTMLエンティティ化を行ったりします。これで有害と思われなかったものは削除されたりHTMLエンティティ化をされないので HTMLインジェクション の対策は行われないのでhtmlspecialchars
を通すこと。
また、XSSのあるSVGの画像には有効ではないので管理された画像を使用する必要がある。
echo htmlspecialchars($this->input->get('html', true), ENT_QUOTES, $this->config->item('charset'));
get
やpost
やget_cookie
の第二引数がxss_clean
をするかどうかになっている
xss_clean
のデフォルト値は$config['global_xss_filtering']
に依存している
リクエストメソッド判定
- POSTとPOST以外のページを同じコントローラのメソッドで実装する例
$data = [];
$data['csrf_token_name'] = $this->security->get_csrf_token_name();
$data['csrf_hash'] = $this->security->get_csrf_hash();
if ($this->input->method() !== 'post')
{
$this->smarty->view('index.html', $data);
return;
}
// POST時の処理
セグメント
パス
- アプリケーション
APPPATH . '../public/uploads/';
バリデーション
$this->load->library('form_validation');
$this->form_validation->set_error_delimiters('', '');// デフォルトはpで囲われるけど無くしたい
$this->form_validation->set_rules(
'password',
'パスワード',
'required|min_length[8]|regex_match[/^(?=.*?[A-Za-z])(?=.*?[0-9])(?=.*?[ -\/:-@\[-`\{-\~])/]',
['regex_match' => '半角英数記号を含めてください'],
);
getの場合
post以外のバリデーションは対象の配列をセットしたら良い
set_rules
の前に設定する必要があります
$this->form_validation->set_data($this->input->get());
配列バリデーション
$this->form_validation->set_rules('input_array[]', '配列', 'required');
画面にエラー表示と入力復帰を行う
const ERROR_KEY = 'error';
入力画面側コントローラ
$cache_prefix = 'login_';
$field_list = [
'id',
'password',
];
$field_list[] = self::ERROR_KEY;
foreach ($field_list as $field) {
$cache_key = $cache_prefix . $field . '_' . 'ユーザーのID等';
$cache = $this->cache->memcached->get($cache_key);
if ($cache !== false) {
$this->cache->memcached->delete($cache_key);
$data[$field] = $cache;
}
}
POST受け取り側コントローラ
$cache_prefix = 'login_';
$field_list = [
'id',
'password',
];
foreach ($this->input->post($field_list, true) as $field => $value) {
if (isset($value)) {
$this->cache->memcached->save($cache_prefix . $field . '_' . 'ユーザーのID等', $value);
}
}
$error_key = $cache_prefix . self::ERROR_KEY . '_' . 'ユーザーのID等';
if ($this->form_validation->run() !== true) {
// バリデーションエラー
$this->cache->memcached->save($error_key, validation_errors());
redirect('入力画面');
}
// 成功時削除
foreach ($this->input->post($field_list, true) as $field => $value) {
if (isset($value)) {
$this->cache->memcached->delete($cache_prefix . $field . 'ユーザーのID等');
}
}
エラーを個別に判定する
$error_list = $this->form_validation->error_array();
// $error_list[フィールド名']でエラーメッセージが入っている配列が取得可能
CSRF対策
特定のページでcsrf protectionを無効にする方法
ファイルダウンロードをPOSTで行った際にそのページのまま再度ダウンロードを行うとcsrf protectionのエラーになってしまうので何とかする。
以下をconfig.phpに追加する
$config['csrf_exclude_uris'] = array(
'download/execute',
);
キャッシュドライバ
$this->cache->memcached->get();
アイテムが存在しない場合、このメソッドは FALSEを返す。
NULLと間違えやすい。
ログ
404
エラーページに前のページに戻るリンクを追加する
views/errors/html/error_404.php
views/errors/html/error_general.php
の
<?php
if (isset($_SERVER['HTTP_REFERER']))
{
echo '<p><a href="' . $_SERVER['HTTP_REFERER'] . '">back to previous page</a></p>';
}
?>
status code
ダウンロード
http://codeigniter.jp/user_guide/3/helpers/download_helper.html
ファイルアップロード
http://codeigniter.jp/user_guide/3/libraries/file_uploading.html
formタグにenctype="multipart/form-data"は忘れずにつけてください。
input type fileは $_POST ではなく $ _FILES に格納されるため、以下では取得できません。
$this->input->post('file_field_name')
複数ファイルには対応していない様子でした。(アップロードするファイルが選択されていません。となる)
$config['upload_path'] = APPPATH . 'uploads/';
$config['file_name'] = 'image';// 拡張子を省略すると元々の拡張子で保存してくれます。
$config['allowed_types'] = 'jpg|png';
$config['overwrite'] = true;
$this->load->library('upload', $config);
// アップロード
if ( ! $this->upload->do_upload('file_field_name'))
{
throw new RuntimeException($this->upload->display_errors());
}
ファイル名をそのまま使う場合は$config['file_name']
を省略します。
省略して後々取得したい場合は$this->upload->data('file_name')
等を使って取得できます。
http://codeigniter.jp/user_guide/3/libraries/file_uploading.html#CI_Upload::data
ディレクトリ・ファイルの削除
http://codeigniter.jp/user_guide/3/helpers/file_helper.html#delete_files
メール
http://codeigniter.jp/user_guide/3/libraries/email.html
Cookie
$config['cookie_secure'] = FALSE;
$config['cookie_httponly'] = true;
httponlyはJavaScriptからセッションIDを取得できないようにするのでtrueにしておくset_cookieの引数でも設定できるけど。
http://codeigniter.jp/user_guide/3/helpers/cookie_helper.html
$this->load->helper('cookie');
set_cookie('name', '名前', $expire_second);
get_cookie('name', true);
クッキーの値又は見つからない場合は NULL
1回のアクセス内でセットと同時に取得はできませんでした。
cookieヘルパーはCI_Input::set_cookie()
のエイリアスということなので
$this->input
に反映されないのかもしれません。
DB
結果の取得の方法
http://codeigniter.jp/user_guide/3/database/results.html#CI_DB_result::result
// array [object]
$result = $this->db
->where('name', $name)
->get($table_name)
->result();
// object|null
$result = $this->db
->where('name', $name)
->get($table_name)
->row();
http://codeigniter.jp/user_guide/3/database/query_builder.html#CI_DB_query_builder::insert
$this->db->insert($table_name, $set_array);
$result = $this->db->affected_rows();
if ($result === 1)
{
$insert_id = $this->db->insert_id();
}
http://codeigniter.jp/user_guide/3/database/db_driver_reference.html?highlight=affected_rows#CI_DB_driver::affected_rows
http://codeigniter.jp/user_guide/3/database/helpers.html#id2
カウント
https://codeigniter.jp/user_guide/3/database/query_builder.html#id6
$this->db->where('name', $name)->count_all_results($table_name);
SELECT count を使用しているわけではない
以下の方が良さそう。
$result = $this->db
->select('count(*) as count')
->where('name', $name)
->get($table_name)
->row();
$count = $result->count;
トランザクション
if ($this->db->trans_begin())
{
try
{
$this->db->trans_commit();
}
catch (\Throwable $error)
{
$this->db->trans_rollback();
}
}
SQLのエスケープ
- クエリービルダ
setやinsertの第三引数をデフォルトにしておけばエスケープされるので気にしなくてよい。
- プリペアドステートメント(CIではサポートされていない)のようなことをする
クエリバインディングを使うとエスケープは自動でやってくれる
$sql = "SELECT * FROM some_table WHERE id IN ? AND status = ? AND author = ?";
$this->db->query($sql, [[3, 6], 'live', 'Rick']);
SELECT * FROM some_table WHERE id IN (3,6) AND status = 'live' AND author = 'Rick'
- SQLを直接書く
変数を用いる場合はエスケープすること
$this->db->escape_str();
$this->db->escape();
escape()
はシングルクォートで囲まれてしまうので使い勝手が悪いです。
$sql = 'SELECT * FROM `table_name` WHERE `name` = ' . $this->db->escape_str($name) . ';';
$query = $this->db->query($sql);
// object[]
$result = $query->result();
http://codeigniter.jp/user_guide/3/database/queries.html#id7
クエリを確認したい場合
$this->db->last_query()
最後に実行されたクエリの文字列を返します
$search_bigram = "あい いう";
$search_bigram = $this->db->escape_str($search_bigram);
// object[]
$result = $this->db
->where('match(`bigram_contents`) against(' . "'" . '+"' . $search_bigram . '" in boolean mode' . "'" . ')');
->get($table_name)
->result();
var_dump($this->db->last_query());
"SELECT * FROM `table_name` WHERE match(`bigram_contents`) against('+"あい いう" in boolean mode');"
クエリービルダーでの注意
where_in
を使う際には第二引数の配列には必ず空配列以外を渡すこと。
where_in
の前に配列のチェックする必要があります。
http://codeigniter.jp/user_guide/3/database/query_builder.html#CI_DB_query_builder::where_in
バッチ実行
-
Batchコントローラを作成し、testメソッドを作成
-
testメソッドの最初には以下を追加してCLIからのみ実行を許可(CLI で実行する)
// ブラウザからのアクセスは禁止
if ( ! is_cli())
{
show_404();
}
- 次のコマンドでバッチの実行が可能
CI_ENV=production php <application root path>/index.php batch test
$_SERVER['CI_ENV']で環境を判定しているのでCI_ENVの設定が必要