WordPressでのログイン通知にDB保存機能を追加する方法

WordPressにおけるセキュリティ向上やユーザー動向の監視を目的に、ログインやログアウト、失敗・プラグイン操作などのイベントを通知する仕組みを導入している方も多いかと思います。今回は、これらの通知に加えて、すべての通知内容をデータベースに保存する機能を追加した構成について紹介します。

本記事で紹介する内容

  • WordPressでログインやログアウト、ログイン失敗、プラグイン有効化・無効化の通知を送る機能
  • 通知対象をChatwork、Slack、メールで切り替え可能
  • すべての通知内容をDBに記録するための構造と処理

ファイル構成

今回使用するのは以下の3ファイルです:

  • multi-login-notifier-extended.php(メインの処理ファイル)
  • settings.php(通知設定画面の出力と保存)
  • db.php(通知ログをデータベースに保存)

multi-login-notifier-extended.php

<?php
/**
 * Plugin Name: Multi Login Notifier Extended
 * Description: WordPressのログイン成功・失敗・ログアウト・プラグインの有効化/無効化をChatwork/Slack/メールで通知。通知種別の制御も可能。
 * Version: 1.5.2
 * Author: Your Name
 */

// WordPress外からのアクセスを防止
defined('ABSPATH') || exit;

// 設定画面とオプション管理の読み込み
require_once plugin_dir_path(__FILE__) . 'includes/settings.php';

// プラグイン有効化時に通知ログ用テーブルを作成
register_activation_hook(__FILE__, 'mln_create_notification_log_table');

/**
 * ログイン成功時の通知
 */
add_action('wp_login', function ($user_login, $user) {
    if (mln_should_skip_notification($user_login, $user->roles)) return;

    $msg = mln_build_message('ログイン成功通知', $user_login, $user->roles);
    mln_send_notifications([
        'type'  => 'login',
        'title' => "ログイン成功: {$user_login}",
        'body'  => $msg,
        'user'  => $user
    ]);
}, 10, 2);

/**
 * ログアウト時の通知(Cookieからユーザー名を取得)
 */
add_action('wp_logout', function () {
    if (isset($_COOKIE[LOGGED_IN_COOKIE])) {
        $cookie_parts = explode('|', $_COOKIE[LOGGED_IN_COOKIE]);
        $username = $cookie_parts[0] ?? null;

        if ($username) {
            $user = get_user_by('login', $username);
            if ($user && !mln_should_skip_notification($user->user_login, $user->roles)) {
                $msg = mln_build_message('ログアウト通知', $user->user_login, $user->roles);
                mln_send_notifications([
                    'type'  => 'logout',
                    'title' => "ログアウト: {$user->user_login}",
                    'body'  => $msg,
                    'user'  => $user
                ]);
            }
        }
    }
});

/**
 * ログイン失敗時の通知(ユーザーが存在しないため user は null)
 */
add_action('wp_login_failed', function ($username) {
    $site = get_bloginfo('name');
    $url  = home_url();
    $ip   = $_SERVER['REMOTE_ADDR'] ?? '';
    $time = current_time('mysql');

    $msg = "[ログイン失敗通知]\n"
         . "ユーザー名: {$username}\n"
         . "日時: {$time}\n"
         . "サイト: {$site}\n"
         . "URL: {$url}\n"
         . "IPアドレス: {$ip}";

    mln_send_notifications([
        'type'  => 'fail',
        'title' => "ログイン失敗: {$username}",
        'body'  => $msg,
        'user'  => null
    ]);
});

/**
 * プラグイン自身の有効化通知
 */
register_activation_hook(__FILE__, function () {
    if (!function_exists('get_plugin_data')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
    $data = get_plugin_data(__FILE__);
    mln_notify_plugin_status_change($data['Name'], $data['Version'], '有効化');
});

/**
 * 他プラグインの有効化/無効化時の通知
 */
add_action('activated_plugin', function ($plugin_path) {
    if ($plugin_path === plugin_basename(__FILE__)) return; // 自プラグインは除外
    mln_handle_plugin_event($plugin_path, '有効化');
});

add_action('deactivated_plugin', function ($plugin_path) {
    if ($plugin_path === plugin_basename(__FILE__)) return; // 自プラグインは除外
    mln_handle_plugin_event($plugin_path, '無効化');
});

/**
 * 通知対象外のユーザー・ロールを判定
 */
function mln_should_skip_notification($username, $roles) {
    $excluded_users = array_map('trim', explode(',', get_option('mln_excluded_users', '')));
    if (in_array($username, $excluded_users, true)) return true;

    $allowed_roles = array_map('trim', explode(',', get_option('mln_allowed_roles', '')));
    if (!empty($allowed_roles) && !array_intersect($roles, $allowed_roles)) return true;

    return false;
}

/**
 * 通知メッセージの共通フォーマットを生成
 */
function mln_build_message($type, $username, $roles) {
    $site = get_bloginfo('name');
    $url  = home_url();
    $ip   = $_SERVER['REMOTE_ADDR'] ?? '';
    $time = current_time('mysql');
    $roles_text = implode(', ', $roles);

    return "[{$type}]\n"
         . "ユーザー: {$username}\n"
         . "権限: {$roles_text}\n"
         . "日時: {$time}\n"
         . "サイト: {$site}\n"
         . "URL: {$url}\n"
         . "IPアドレス: {$ip}";
}

/**
 * プラグインの有効化/無効化イベントを処理
 */
function mln_handle_plugin_event($plugin_path, $status = '有効化') {
    if (!function_exists('get_plugin_data')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
    $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path;
    if (!file_exists($plugin_file)) return;
    $plugin_data = get_plugin_data($plugin_file);
    mln_notify_plugin_status_change($plugin_data['Name'], $plugin_data['Version'], $status);
}

/**
 * プラグインの状態変化通知(Chatwork, Slack, メール, DB)
 */
function mln_notify_plugin_status_change($name, $version, $status = '有効化') {
    $site = get_bloginfo('name');
    $url  = home_url();
    $time = current_time('mysql');

    $msg = "[プラグイン{$status}通知]\n"
         . "サイト: {$site}\n"
         . "プラグイン: {$name} ({$version})\n"
         . "URL: {$url}\n"
         . "時間: {$time}";

    mln_send_notifications([
        'type'  => 'plugin',
        'title' => "プラグイン{$status}: {$name}",
        'body'  => $msg,
        'user'  => wp_get_current_user()
    ]);
}

/**
 * 通知の送信本体(通知先制御 & DB保存)
 */
function mln_send_notifications($params) {
    $type  = $params['type'];
    $body  = $params['body'];
    $title = $params['title'];
    $user  = $params['user'];

    $sent_to = [];

    // Chatwork通知
    if (
        get_option('mln_chatwork_enabled') === '1' &&
        get_option("mln_chatwork_notify_{$type}") === '1'
    ) {
        $token = get_option('mln_chatwork_token');
        $room  = get_option('mln_chatwork_room_id');
        if ($token && $room) {
            wp_remote_post("https://api.chatwork.com/v2/rooms/{$room}/messages", [
                'headers' => ['X-ChatWorkToken' => $token],
                'body'    => ['body' => $body],
            ]);
            $sent_to[] = 'chatwork';
        }
    }

    // Slack通知
    if (
        get_option('mln_slack_enabled') === '1' &&
        get_option("mln_slack_notify_{$type}") === '1'
    ) {
        $webhook = get_option('mln_slack_webhook_url');
        if ($webhook) {
            wp_remote_post($webhook, [
                'headers' => ['Content-Type' => 'application/json'],
                'body'    => json_encode(['text' => $body]),
            ]);
            $sent_to[] = 'slack';
        }
    }

    // メール通知
    if (
        get_option('mln_email_enabled') === '1' &&
        get_option("mln_email_notify_{$type}") === '1'
    ) {
        $to = get_option('mln_email_to');
        if ($to) {
            wp_mail($to, $title, $body);
            $sent_to[] = 'email';
        }
    }

    // DBログ記録(ユーザーが存在する場合のみ)
    if ($user && isset($user->user_login, $user->roles)) {
        $username = $user->user_login;
        $roles    = $user->roles;
        $notified = implode(',', $sent_to);
        if (!function_exists('mln_log_notification_to_db')) {
            require_once plugin_dir_path(__FILE__) . 'includes/db.php';
        }
        mln_log_notification_to_db($type, $username, implode(',', $roles), $body, $notified);
    }
}

includes/settings.php

<?php
defined('ABSPATH') || exit;

add_action('admin_menu', function () {
    add_options_page(
        'Login Notifier Settings',
        'Login Notifier',
        'manage_options',
        'mln-settings',
        'mln_render_settings_page'
    );
});

function mln_render_settings_page() {
    ?>
    <div class="wrap">
        <h1>Multi Login Notifier Extended 設定</h1>
        <form method="post" action="options.php">
            <?php
            settings_fields('mln_settings');
            do_settings_sections('mln-settings');
            submit_button();
            ?>
        </form>
    </div>
    <?php
}

add_action('admin_init', function () {
    // 共通設定
    register_setting('mln_settings', 'mln_allowed_roles');
    register_setting('mln_settings', 'mln_excluded_users');

    // Chatwork
    register_setting('mln_settings', 'mln_chatwork_enabled');
    register_setting('mln_settings', 'mln_chatwork_token');
    register_setting('mln_settings', 'mln_chatwork_room_id');
    register_setting('mln_settings', 'mln_chatwork_notify_login');
    register_setting('mln_settings', 'mln_chatwork_notify_logout');
    register_setting('mln_settings', 'mln_chatwork_notify_fail');
    register_setting('mln_settings', 'mln_chatwork_notify_plugin');

    // Slack
    register_setting('mln_settings', 'mln_slack_enabled');
    register_setting('mln_settings', 'mln_slack_webhook_url');
    register_setting('mln_settings', 'mln_slack_notify_login');
    register_setting('mln_settings', 'mln_slack_notify_logout');
    register_setting('mln_settings', 'mln_slack_notify_fail');
    register_setting('mln_settings', 'mln_slack_notify_plugin');

    // Email
    register_setting('mln_settings', 'mln_email_enabled');
    register_setting('mln_settings', 'mln_email_to');
    register_setting('mln_settings', 'mln_email_notify_login');
    register_setting('mln_settings', 'mln_email_notify_logout');
    register_setting('mln_settings', 'mln_email_notify_fail');
    register_setting('mln_settings', 'mln_email_notify_plugin');

    // セクション
    add_settings_section('mln_main', '通知設定', null, 'mln-settings');

    // Chatwork 設定
    mln_add_checkbox('mln_chatwork_enabled', 'Chatwork通知を有効化');
    mln_add_text('mln_chatwork_token', 'Chatwork APIトークン');
    mln_add_text('mln_chatwork_room_id', 'ChatworkルームID');
    mln_add_checkbox('mln_chatwork_notify_login', 'ログイン成功通知');
    mln_add_checkbox('mln_chatwork_notify_logout', 'ログアウト通知');
    mln_add_checkbox('mln_chatwork_notify_fail', 'ログイン失敗通知');
    mln_add_checkbox('mln_chatwork_notify_plugin', 'プラグイン操作通知');

    // Slack 設定
    mln_add_checkbox('mln_slack_enabled', 'Slack通知を有効化');
    mln_add_text('mln_slack_webhook_url', 'Slack Webhook URL');
    mln_add_checkbox('mln_slack_notify_login', 'ログイン成功通知');
    mln_add_checkbox('mln_slack_notify_logout', 'ログアウト通知');
    mln_add_checkbox('mln_slack_notify_fail', 'ログイン失敗通知');
    mln_add_checkbox('mln_slack_notify_plugin', 'プラグイン操作通知');

    // メール通知設定
    mln_add_checkbox('mln_email_enabled', 'メール通知を有効化');
    mln_add_text('mln_email_to', '通知先メールアドレス');
    mln_add_checkbox('mln_email_notify_login', 'ログイン成功通知');
    mln_add_checkbox('mln_email_notify_logout', 'ログアウト通知');
    mln_add_checkbox('mln_email_notify_fail', 'ログイン失敗通知');
    mln_add_checkbox('mln_email_notify_plugin', 'プラグイン操作通知');

    // ユーザー制御設定
    mln_add_text('mln_allowed_roles', '通知対象のロール(カンマ区切り)');
    mln_add_text('mln_excluded_users', '通知除外ユーザー名(カンマ区切り)');
});

/**
 * 入力欄ヘルパー(テキスト)
 */
function mln_add_text($option_name, $label) {
    add_settings_field($option_name, $label, function () use ($option_name) {
        $value = esc_attr(get_option($option_name));
        echo "<input type='text' name='{$option_name}' value='{$value}' class='regular-text' />";
    }, 'mln-settings', 'mln_main');
}

/**
 * 入力欄ヘルパー(チェックボックス)
 */
function mln_add_checkbox($option_name, $label) {
    add_settings_field($option_name, $label, function () use ($option_name) {
        $value = get_option($option_name);
        echo "<input type='checkbox' name='{$option_name}' value='1' " . checked(1, $value, false) . " />";
    }, 'mln-settings', 'mln_main');
}

includes/db.php

<?php
/**
 * DB操作用ファイル
 * 通知ログテーブルの作成とログ保存処理を提供
 */

if (!defined('ABSPATH')) exit;

/**
 * 通知ログテーブルの作成
 * プラグイン有効化時、または初回通知時に実行
 */
function mln_create_notification_log_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'mln_notification_logs';

    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        event_type VARCHAR(50) NOT NULL,
        user_login VARCHAR(60) DEFAULT '',
        role VARCHAR(100) DEFAULT '',
        message TEXT NOT NULL,
        notified_to TEXT DEFAULT '',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id)
    ) {$charset_collate};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

/**
 * 通知ログをDBに保存
 * @param string       $event_type   イベント種別(login, logout, fail, plugin_activate など)
 * @param string       $user_login   ユーザー名(取得できない場合は空)
 * @param string       $role         ロール名(カンマ区切り)
 * @param string       $message      通知内容
 * @param string|array $notified_to  通知先(chatwork, slack, email など)
 */
function mln_log_notification_to_db($event_type, $user_login, $role, $message, $notified_to) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'mln_notification_logs';

    // テーブルが存在しない場合は作成
    if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") !== $table_name) {
        mln_create_notification_log_table();
    }

    // 通知先が配列の場合はカンマで結合、文字列の場合はそのまま使用、その他は空文字
    if (is_array($notified_to)) {
        $notified_str = implode(',', $notified_to);
    } elseif (is_string($notified_to)) {
        $notified_str = $notified_to;
    } else {
        $notified_str = '';
    }

    // レコードを挿入
    $wpdb->insert(
        $table_name,
        [
            'event_type'  => $event_type,
            'user_login'  => $user_login,
            'role'        => $role,
            'message'     => $message,
            'notified_to' => $notified_str,
            'created_at'  => current_time('mysql', 1)
        ],
        [
            '%s', '%s', '%s', '%s', '%s', '%s'
        ]
    );
}

DBテーブルの構成

作成されるテーブルは以下のような構造です:

カラム名説明
idBIGINT主キー(AUTO_INCREMENT)
event_typeVARCHAR(50)login / logout / plugin など
user_loginVARCHAR(60)ユーザー名
roleVARCHAR(100)ユーザー権限
messageTEXT通知本文
notified_toTEXT通知先(カンマ区切り)
created_atDATETIME生成日時(CURRENT_TIMESTAMP)

使い方

  1. 通常のプラグインと同様に wp-content/plugins/ に3ファイルを配置してください。
  2. WordPress管理画面からプラグインを有効化します。
  3. 通知対象(Chatwork/Slack/メール)のON/OFFや通知種別を設定ページから制御できます。

おわりに

通知を送るだけではなく、データベースに記録しておくことで後からログとして見返したり、分析に活用することも可能になります。セキュリティログやトラブル時の原因調査にも有効です。

シンプルな構成で、既存の通知プラグインに追加機能を加えたい場合にも活用できます。

コメント

タイトルとURLをコピーしました