顯示具有 Codeigniter 標籤的文章。 顯示所有文章
顯示具有 Codeigniter 標籤的文章。 顯示所有文章

2016/12/18

Google reCAPTCHA On Codeigniter

前言

為了避免網站被 robot 掃模做重複性的動作,例如加入會員,申請忘記密碼等等,我們會放一個必須人為輸入的欄位並且讓 robot 難以辨識破解,常見的會有文字以及數字的圖像化輸入,但有的機器人如果拿到規律以及足夠的樣本,純文字的輸入方式是可能被破解的,Google 出的 reCaptcha 屬於圖像化的選擇驗證,不管是排列位置或是種類的多樣性都難以去破解,在這邊示範一下怎麼整合 ci 使用

實做

套件安裝

首先安裝 google/recaptcha 這個套件,我在 codeigniter 上有使用 composer,在 config/config.php 下可以打開,使用方法跟在 Laravel 上一樣

程式碼

application/controllers/Recaptcha.php
<?php

defined('BASEPATH') OR exit('No direct script access allowed');

class Recaptcha extends CI_Controller
{
    public function index()
    {
        $this->load->helper('form');
        $this->load->helper('url');
        $this->load->view('recaptcha/form');
    }

    public function result()
    {
        $this->load->library('form_validation');

        $status = 'done';
        $message = 'done';
        $secret = '6LfsIQ8UAAAAAOveAzcFb6A_3Yiv_QdfR-qo5qld';

        try {
            $this->form_validation->set_rules('username', 'username', 'required');
            $this->form_validation->set_rules('g-recaptcha-response', 'captcha', 'required');

            if (!$this->form_validation->run()) {
                throw new Exception(validation_errors(null, null));
            }

            $gRecaptchaResponse = $this->input->post('g-recaptcha-response');
            $remoteIp = $this->input->ip_address();
            $recaptcha = new \ReCaptcha\ReCaptcha($secret);
            $resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp);

            if (!$resp->isSuccess()) {
                $errors = $resp->getErrorCodes();
                throw new Exception(current($errors));
            }
        } catch (Exception $e) {
            $status = 'fail';
            $message = $e->getMessage();
        }

        $this->output
            ->set_content_type('application/json')
            ->set_output(json_encode(compact('status', 'message')));
    }
}
application/views/recaptcha/form.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>reCAPTCHA Demo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src='https://www.google.com/recaptcha/api.js'></script>
    <script type="text/javascript" charset="utf-8">
    var baseUrl = "<?php echo base_url(); ?>";

    $(function() {
        $('#recaptcha_form').on('submit', function() {
            var data = $(this).serialize();

            $.post(baseUrl + 'recaptcha/result', data, function(response) {
                alert(response.message);

                if (response.status === 'done') {
                    grecaptcha.reset();
                }
            }, 'json');

            return false;
        });
    });
    </script>
</head>
<body>
    <?php echo form_open('recaptcha/result', ['id' => 'recaptcha_form']); ?>
    <div><?php echo form_input('uername', 'chan'); ?></div>
    <div class="g-recaptcha" data-sitekey="6LfsIQ8UAAAAAFmYCwSLpfKgWtqK1SlVfMcPn2Q6"></div>
    <div><?php echo form_submit('go', 'go'); ?></div>
    <?php echo form_close(); ?>
</body>
</html>

使用 reCAPTCHA 要先去申請 key 跟 secret,這部份我就不講解了,這份程式碼主要功能就是驗證是否有輸入 username 以及 reCAPTCHA,有的話再進一步驗證 captcha 正確性,如果你不想使用套件的錯誤訊息,那一旦報錯你只要顯示 captcha 錯誤即可,有些表單送出後不一定會換頁,所以我加了個 script 如果偵測到這次的行為正確的時候 refresh 一個新的 captcha,語法的使用時機可以看使用者的使用情況去調整搭配

2016/11/17

Query Log

sql 存取 log 可以用來查尋所有的操作軌跡,這邊使用 codeigniter 的 hooks,來達成自動記錄 query 的功能。

資料庫

CREATE TABLE `query_logs` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`type` CHAR(10) NOT NULL,
`location` VARCHAR(255) NOT NULL,
`content` TEXT NOT NULL,
`admin_id` INT NOT NULL,
INDEX(`admin_id`),
`created_at` DATETIME NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;

type 用來記錄 sql 的處理方式,例如說是 update 還是 delete,content 的部分存整句的語法,admin_id 是我後台的管理者 id,我這邊的範例只打算記錄後台操作 insert, update, delete 三種行為,所以我用 admin_id 當作是否要記錄的依據,如果要做更細的設定可以再行配置。

設定

application/config/autoload.php
$autoload['libraries'] = array('database', 'session');
$autoload['config'] = array('site');
application/config/hooks.php
$hook['post_controller'] = [
    'class' => 'QueryLogger',
    'function' => 'save',
    'filename' => 'QueryLogger.php',
    'filepath' => 'hooks',
    'params' => ''
];
application/config/site.php
$config['query_types'] = [
    'insert',
    'update',
    'delete'
];

我目前只紀錄了這三個事件,如果有其他需求可以透過 config 增減

Hook 功能

application/hooks/QueryLogger.php
<?php

if (!defined('BASEPATH')) exit('No direct script access allowed');

class QueryLogger
{
    private $ci;
    private $types;

    public function __construct()
    {
        $this->ci =& get_instance();
        $this->types = $this->ci->config->item('query_types');
    }

    public function save()
    {
        if ($this->can_saved()) {
            foreach ($this->ci->db->queries as $query) {
                $query = str_replace("\n", '', $query);
                $type = $this->get_type($query);

                if ($type !== '') {
                    $this->ci->db->insert('query_logs', [
                        'type' => $type,
                        'content' => $query,
                        'location' => $this->ci->router->fetch_class().'/'.$this->ci->router->fetch_method(),
                        'admin_id' => $this->ci->session->admin_id,
                        'created_at' => date('Y-m-d H:i:s')
                    ]);
                }
            }
        }

    }

    public function can_saved()
    {
        if (count($this->ci->db->queries) > 0 && $this->ci->session->has_userdata('admin_id')) {
            return true;
        }

        return false;
    }

    public function get_type($query)
    {
        return implode('', array_filter($this->types, function ($type) use ($query) {
            return stristr($query, $type);
        }));
    }
}

2016/11/04

Codeigniter image_helper

前言

今天介紹兩個縮圖的 helper,使用的套件是 ImageWorkshop,以下面這兩張圖片解說。

500x300

300x500

程式碼

application/contollers/Image.php
<?php

class Image extends CI_Controller
{
    public function index()
    {
        $this->load->helper('image_helper');

        $this->load->view('image');
    }
}
application/helpers/image_helper.php
<?php

use PHPImageWorkshop\ImageWorkshop;

if (!function_exists('fit_thumb')) {
    function fit_thumb($path, $width = 150, $height = 0)
    {
        if (!file_exists($path)) {
            return 'assets/imgs/file_not_exists.png';
        }

        if ($height == 0) {
            $height = $ratio = $width;
        } else {
            $ratio = ($width < $height) ? $height : $width;
        }

        $paths = pathinfo($path);
        $thumbName = "{$paths['filename']}_fit_{$width}x{$height}.{$paths['extension']}";
        $thumbPath = $paths['dirname'].'/thumb/';
        $fullThumbPath = $thumbPath.$thumbName;

        if (file_exists($fullThumbPath)) {
            return $fullThumbPath;
        }

        $layer = ImageWorkshop::initFromPath($path, true);
        $layer->resizeByLargestSideInPixel($ratio, true);

        $layer->cropInPixel($width, $height, 0, 0, 'MT');
        $layer->save($thumbPath, $thumbName);

        return $fullThumbPath;
    }
}

if (!function_exists('thumb')) {
    function thumb($path, $ratio = 150, $bySide = 'large')
    {
        if (!file_exists($path)) {
            return 'assets/imgs/file_not_exists.png';
        }

        $paths = pathinfo($path);

        switch ($bySide) {
            case 'large':
                $thumbName = "{$paths['filename']}_thumb_large_{$ratio}.{$paths['extension']}";
                break;
            case 'narrow':
                $thumbName = "{$paths['filename']}_thumb_narrow_{$ratio}.{$paths['extension']}";
                break;
        }

        $thumbPath = $paths['dirname'].'/thumb/';
        $fullThumbPath = $thumbPath.$thumbName;

        if (file_exists($fullThumbPath)) {
            return $fullThumbPath;
        }

        $layer = ImageWorkshop::initFromPath($path, true);

        switch ($bySide) {
            case 'large':
                $layer->resizeByLargestSideInPixel($ratio, true);
                break;
            case 'narrow':
                $layer->resizeByNarrowSideInPixel($ratio, true);
                break;
        }

        $layer->save($thumbPath, $thumbName);

        return $fullThumbPath;
    }
}

if (!function_exists('img_upload')) {
    function img_upload($path = '/', $image = '', $maxWidth = 3000)
    {
        $status = 'done';
        $message = '';
        $allows = ['jpeg', 'png', 'gif'];
        $file = $_FILES[$image];
        $fileName = $file['name'];
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
        $newName = date('YmdHis').rand(1000, 9999).'.'.$ext;
        $map = function ($item) {
            return "image/{$item}";
        };

        try {
            if (empty($fileName)) {
                throw new Exception('請選擇檔案');
            }

            $type = $file['type'];
            $tmpName = $file['tmp_name'];

            if (!in_array($type, array_map($map, $allows))) {
                throw new Exception('檔案格式錯誤,僅接受'.implode(', ', $allows));
            }

            $layer = ImageWorkshop::initFromPath($tmpName, true);
            list($width, $height) = getimagesize($tmpName);

            if ($width > $maxWidth) {
                $layer->resizeInPixel($maxWidth, null, true);
            }

            $layer->save($path, $newName);
        } catch (Exception $e) {
            $status = 'fail';
            $message = $e->getMessage();
        }

        return [
            'status' => $status,
            'image' => $newName,
            'message' => $message,
        ];
    }
}

if (!function_exists('image_delete')) {
    function image_delete($path, $image)
    {
        $path = rtrim($path, '/').'/';
        @unlink($path.$image);
        $fileName = pathinfo($image, PATHINFO_FILENAME);
        $thumbPath = $path.'thumb/';
        $thumbs = directory_map($thumbPath);

        foreach ($thumbs as $thumb) {
            $body = explode('_', $thumb)[0];

            if ($body === $fileName) {
                @unlink($thumbPath.$thumb);
            }
        }
    }
}
application/views/image.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Image Helper</title>
</head>
<body>
    <h3>500x300</h3>
    <img src="<?php echo $landscape; ?>">
    <h3>300x500</h3>
    <img src="<?php echo $portrait; ?>">

    <h3>fit_thumb</h3>
    <img src="<?php echo fit_thumb($landscape, 150); ?>">
    <img src="<?php echo fit_thumb($landscape, 150, 200); ?>">
    <img src="<?php echo fit_thumb($portrait, 150); ?>">
    <img src="<?php echo fit_thumb($portrait, 200, 150); ?>">

    <h3>thumb</h3>
    <img src="<?php echo thumb($landscape, 150); ?>">
    <img src="<?php echo thumb($landscape, 150, 'narrow'); ?>">
    <img src="<?php echo thumb($portrait, 150); ?>">
    <img src="<?php echo thumb($portrait, 150, 'narrow'); ?>">
</body>
</html>

Demo

fit_thumb

fit_thumb 如果沒有帶入第三個參數便會擷取該圖形為正方形,如果有帶入的話便會照比例擷取。

500x300

fit_thumb($landscape, 150)

150x150

fit_thumb($landscape, 150, 200)

150x200

300x500

fit_thumb($portrait, 150)

150x150

fit_thumb($portrait, 200, 150)

200x150

thumb

thumb 功能會依照傳入的比例參數將圖片依照參數等比例縮放,第二個參數可以決定從寬邊還是窄邊做縮放

500x300

thumb($landscape, 150)

150x90

thumb($landscape, 150, 'narrow')

250x150

300x500

thumb($portrait, 150)

90x150

thumb($portrait, 150, 'narrow')

150x250

img_upload

img_upload 這個 function 可以將你上傳的圖片縮圖後存入,一般來說網頁會顧慮的是寬不要破壞,因此程式碼會以寬度為主來做縮圖。

2016/11/01

Codeigniter file_manager helper

檔案上傳是蠻常使用的功能,ci 裡面有內建檔案上傳的功能,但我們當然不想記得繁複的呼叫方式,寫成 helper 來使用是最方便的,今天 news 當作範例示範一下寫法。

news table
CREATE TABLE `news` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL,
`file_name` VARCHAR(100) NOT NULL,
`real_name` VARCHAR(100) NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
application/controllers/News.php
class News extends CI_Controller
{
    public function index()
    {
        $this->load->helper('form');
        $this->load->view('news');
    }

    public function save()
    {
        $this->load->database();
        $this->load->helper('file_manager');
        $data = [];

        if (has_upload('file')) {
            $upload = file_upload('file', 'assets/uploads/news');

            if ($upload['status'] == 'fail') {
                die($upload['message']);
            } else {
                $data['file_name'] = $upload['fileName'];
                $data['real_name'] = $upload['realName'];
            }
        }

        $data['name'] = uniqid();
        $this->db->insert('news', $data);
    }

    public function download($id)
    {
        $this->load->database();
        $this->load->helper('file_manager');
        $row = $this->db->get_where('news', ['id' => $id])->row_array();

        force_download("assets/uploads/news/{$row['file_name']}", $row['real_name']);
    }
}

index method 就是簡單的建立一個表單,傳送 name 跟 file 這兩的 field 到 save 儲存,這邊做的都是最簡單的示範,實際情況還是資照需求去修改,但回傳的內容應該都可以應付其他的條件,download 的部份就是調用 file_manager helper 下載原始的檔案,若非必要,盡量不要在 helper 裡面做 db connection,因為 helper 很有可能會在迴圈裡面出現,這樣 db query 數量會隨資料量增加。

application/views/news.php
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>News</title>
    </head>
    <body>
        <?php echo form_open_multipart('news/save'); ?>
        <?php echo form_input('name', uniqid()); ?>
        <?php echo form_upload('file'); ?>
        <?php echo form_submit('go', 'go'); ?>
        <?php echo form_close(); ?>
    </body>
</html>

news 的 view,簡單的使用 form helper 生成。

application/helpers/file_manager_helper.php
if (!function_exists('file_upload')) {
    function file_upload($file, $path, array $allows = [])
    {
        $ci =& get_instance();
        $status = 'done';
        $message = '';
        $fileName = '';
        $realName = '';
        $config['upload_path'] = $path;
        $config['allowed_types'] = (count($allows) > 0) ? implode('|', $allows) : '*';
        $config['encrypt_name'] = true;
        $ci->load->library('upload', $config);

        if (!$ci->upload->do_upload($file)) {
            $message = $ci->upload->display_errors();
            $status = 'fail';
        } else {
            $fileData = $ci->upload->data();
            $fileName = $fileData['file_name'];
            $realName = $fileData['client_name'];
        }

        return compact('status', 'message', 'fileName', 'realName');
    }
}

if (!function_exists('force_download')) {
    function force_download($path, $realName)
    {
        $originFile = urlencode($realName);
        $realFile = $path;

        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Content-Type: application/force-download");
        header("Content-Disposition: attachment; filename=".$originFile);
        header("Content-Description: File Transfer");
        @readfile($realFile);
    }
}

if (!function_exists('has_upload')) {
    function has_upload($file)
    {
        if (isset($_FILES[$file]) && is_uploaded_file($_FILES[$file]['tmp_name'])) {
            return true;
        }

        return false;
    }
}

file_upload function 調用了 upload 的 library,因為主要是傳檔案,所以預設 allows 為全部,當然也有先寫好參數可以傳遞,印象中 codeigniter 的上傳會檢查 mime,所以 allow 的內容可能要符合 mime 的檔名規範,如果要上傳的是圖片的東西,可以用這個 function 去改出一個 image_upload 的 function,再把跟圖片比較有關連的內容放進去,在上傳的時候我們會將檔名使用英文編碼代替,但原始的檔名會一起存到 database,在很多 Linux server 檔案名稱使用非英文的話會無法讀取,所以最好是用純英數去儲存檔案,因為 codeigniter 的 input helper 本身沒有支援 file 的內容,所以寫了一個 has_upload 的 function 來檢查這次的傳輸有沒有上傳檔案。

force_download 這個 function 只要將檔案位置跟原始檔名帶入,便可以下載原始的檔案,即便是中文也沒問題。

2015/10/07

CodeIgniter Migration

一般的程式設計師設計資料庫常常會使用 PHPMyAdmin 或者是一些軟體直接在 DB 上做表單的建立或管理,一個人的專案可能還 ok,但如果遇到 co-work,或者是測試機與正式機不同機器,很難避免粗心漏掉內容的問題,最好的解決方案就是用程式產生相關的內容,PHP 兩個熱門的框架 Laravel 以及 CodeIgniter 都有提供 Database Migration 的功能,今天來初探 CodeIgniter 的 Migration 部分。

CodeIgniter 所有 Migration 的內容只要看過上面兩個連結就可以做完,讓我們來 step by step 案例。

前置作業

application/config/migration.php

這個檔案是設定 migration config 的位置,有幾個重點要設定

$config['migration_enabled'] = true;

將 miagration_enabled 的部分開啟。

$config['migration_type'] = 'sequential';

修改 migration_type 為 sequential,看個人習慣,這個設定是指 migration 的檔案取名方式,如果是 timestamp 檔名取法則用日期當做順序,例: 20121031104401_add_blog.php,而 sequential 的話就是按照檔名排序,例:001_add_blog.php,其他設定如 miagration 資料夾位置,這邊我用 default,所以我們建立一個目錄 application/migrations/。

實做

application/migrations/001_create_users_table.php
class Migration_Create_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->add_field([
            'id' => [
                'type' => 'int',
                'unsigned' => true,
                'auto_increment' => true,
            ],
            'username' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'password' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'gender' => [
                'type' => 'tinyint',
                'constraint' => 1
            ],
            'created_at' => [
                'type' => 'datetime'
            ],
            'updated_at TIMESTAMP DEFAULT NOW() ON UPDATE NOW()'
        ]);

        $this->dbforge->add_key('id', true);
        $this->dbforge->create_table('users');
    }

    public function down()
    {
        $this->dbforge->drop_table('users');
    }
}

class name 一定要是 Migration_ 加上字首大寫並且去掉序列的檔名,不然不會動,up 是建立的部分,down 則是給 rollback 用的,所以我們這邊的 up 是 create 某個 table 的話,down 就是 drop 某個 table,以此類推,add_key 的部分有加 true 會是 primary key,沒有的話就是一般的 index,基本上欄位的設定就是把 sql 的欄位建立方式填進去並且給值,constraint 不給的話預設會是最大,如果在 MySQL 裡面試是非題的話就是用 true false 給值,像是 null => false 則預設不能 null,如果沒下的話預設值看 MySQL 怎麼設定。

application/controllers/Migrate.php
class Migrate extends CI_Controller
{
    public function start()
    {
        $this->load->library('migration');

        if ($this->migration->latest() === false) {
            echo $this->migration->error_string();
        } else {
            echo 'migrated'.PHP_EOL;
        }
    }
}

我們建立一個 controller 來執行 migration,網路上有很多方法可以讓這個 controller 只能透過 cli 執行,但我實際跑過,如果你是下 $this->migration->latest() 的話,從 browser 重複執行並不會影響建構好的 table,所以我是沒有打算放,而且客戶常常都只有給 FTP 哪有辦法走 cli 模式。

建立完畢後我在本機的 cli 跑 php index.php migrate start 得到:

"chan" Sid: S-1-5-21-759891990-2490853520-3465201229-1000
migrated

打開資料庫看得到了 migrations 以及 users table,這樣就是順利完成了,接下來我們來模擬幾個場景。

application/migrations/002_add_ip_to_users_table.php
class Migration_Add_ip_to_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'ip' => [
                'type' => 'varchar',
                'constraint' => 100,
                'null' => false,
            ]
        ];

        $this->dbforge->add_column('users', $field, 'password');
    }

    public function down()
    {
        $this->dbforge->drop_column('users', 'ip');
    }
}

這個場景就是在 users 裡面增加一個欄位,add_column 的第三個欄位等於 MySQL 的 after。

application/migrations/003_drop_gender_from_users_table
class Migration_Drop_gender_from_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->drop_column('users', 'gender');
    }
}

從 users table 移掉 gender 這個 column。

004_modify_password_from_users_table.php
class Migration_Modify_password_from_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'password' => [
                'name' => 'password',
                'type' => 'varchar',
                'constraint' => 100,
            ]
        ];

        $this->dbforge->modify_column('users', $field);
    }
}

將 password 的長度增加到 100,在 MySQL 如果要改一個欄位的內容 alter 的語法你必須把其他欄位設定都在打一次,所以不要只有打 'constraint' => 100 而已,這樣會出事。