Halloween Sale 15% off Ends in See the plans
PHP

Activity Log in Its Own Database Table

Records logins, failed logins, content changes, plugin, theme and user changes and edits to key settings in a dedicated database table, with a searchable, paginated log under Tools > Activity Log and automatic cleanup.

Don’t have WPIDE yet? Install it on your site

Code

// Days to keep log entries, and settings whose changes are logged.
$wpide_log_days    = 90;
$wpide_log_options = array( 'siteurl', 'home', 'admin_email', 'users_can_register', 'default_role', 'blog_public', 'permalink_structure' );

$wpide_log_table = function () {
    global $wpdb;
    return $wpdb->prefix . 'wpide_activity';
};

$wpide_log = function ( $action, $object = '', $details = '', $user_id = null ) use ( $wpide_log_table ) {
    global $wpdb;

    if ( '1' !== get_option( 'wpide_activity_db' ) ) {
        return;
    }

    $user_id = null === $user_id ? get_current_user_id() : (int) $user_id;
    $user    = $user_id ? get_userdata( $user_id ) : false;

    $wpdb->insert( $wpide_log_table(), array(
        'created'    => current_time( 'mysql', true ),
        'user_id'    => $user_id,
        'user_login' => $user ? $user->user_login : '',
        'ip'         => isset( $_SERVER['REMOTE_ADDR'] ) ? substr( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ), 0, 45 ) : '',
        'action'     => substr( $action, 0, 40 ),
        'object'     => substr( (string) $object, 0, 190 ),
        'details'    => substr( (string) $details, 0, 1000 ),
    ) );
};

add_action( 'admin_init', function () use ( $wpide_log_table ) {
    global $wpdb;

    if ( '1' === get_option( 'wpide_activity_db' ) || ! current_user_can( 'manage_options' ) ) {
        return;
    }

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( 'CREATE TABLE ' . $wpide_log_table() . " (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        created datetime NOT NULL,
        user_id bigint(20) unsigned NOT NULL DEFAULT 0,
        user_login varchar(60) NOT NULL DEFAULT '',
        ip varchar(45) NOT NULL DEFAULT '',
        action varchar(40) NOT NULL,
        object varchar(190) NOT NULL DEFAULT '',
        details text NOT NULL,
        PRIMARY KEY  (id),
        KEY created (created),
        KEY action (action)
    ) " . $wpdb->get_charset_collate() . ';' );
    update_option( 'wpide_activity_db', '1' );
} );

add_action( 'wp_login', function ( $login, $user ) use ( $wpide_log ) {
    $wpide_log( 'login', $login, '', $user->ID );
}, 10, 2 );
add_action( 'wp_login_failed', function ( $login ) use ( $wpide_log ) {
    $wpide_log( 'login_failed', sanitize_user( $login ), '', 0 );
} );
add_action( 'transition_post_status', function ( $new, $old, $post ) use ( $wpide_log ) {
    if ( $new === $old || wp_is_post_revision( $post ) || in_array( $new, array( 'auto-draft', 'inherit' ), true ) || ! post_type_exists( $post->post_type ) || ! is_post_type_viewable( $post->post_type ) ) {
        return;
    }
    $wpide_log( 'post_' . $new, $post->post_type . ' #' . $post->ID, $post->post_title );
}, 10, 3 );
add_action( 'before_delete_post', function ( $post_id, $post ) use ( $wpide_log ) {
    if ( $post && is_post_type_viewable( $post->post_type ) && 'revision' !== $post->post_type ) {
        $wpide_log( 'post_deleted', $post->post_type . ' #' . $post_id, $post->post_title );
    }
}, 10, 2 );
foreach ( array( 'activated_plugin' => 'plugin_activated', 'deactivated_plugin' => 'plugin_deactivated', 'deleted_plugin' => 'plugin_deleted' ) as $wpide_hook => $wpide_action ) {
    add_action( $wpide_hook, function ( $plugin ) use ( $wpide_log, $wpide_action ) {
        $wpide_log( $wpide_action, $plugin );
    } );
}
add_action( 'switch_theme', function ( $name ) use ( $wpide_log ) {
    $wpide_log( 'theme_switched', $name );
} );
add_action( 'upgrader_process_complete', function ( $upgrader, $extra ) use ( $wpide_log ) {
    if ( ! empty( $extra['type'] ) && ! empty( $extra['action'] ) ) {
        $items = isset( $extra['plugins'] ) ? $extra['plugins'] : ( isset( $extra['themes'] ) ? $extra['themes'] : array() );
        $wpide_log( $extra['type'] . '_' . $extra['action'], implode( ', ', (array) $items ) );
    }
}, 10, 2 );
add_action( 'user_register', function ( $user_id ) use ( $wpide_log ) {
    $user = get_userdata( $user_id );
    $wpide_log( 'user_created', $user ? $user->user_login : '#' . $user_id, $user ? implode( ',', $user->roles ) : '' );
} );
add_action( 'delete_user', function ( $user_id ) use ( $wpide_log ) {
    $user = get_userdata( $user_id );
    $wpide_log( 'user_deleted', $user ? $user->user_login : '#' . $user_id );
} );
add_action( 'set_user_role', function ( $user_id, $role, $old ) use ( $wpide_log ) {
    if ( $old ) {
        $user = get_userdata( $user_id );
        $wpide_log( 'role_changed', $user ? $user->user_login : '#' . $user_id, implode( ',', (array) $old ) . ' -> ' . $role );
    }
}, 10, 3 );
add_action( 'updated_option', function ( $option, $old, $new ) use ( $wpide_log, $wpide_log_options ) {
    if ( in_array( $option, $wpide_log_options, true ) ) {
        $wpide_log( 'setting_changed', $option, wp_json_encode( $old ) . ' -> ' . wp_json_encode( $new ) );
    }
}, 10, 3 );

add_action( 'init', function () {
    if ( ! wp_next_scheduled( 'wpide_activity_cleanup' ) ) {
        wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wpide_activity_cleanup' );
    }
} );
add_action( 'wpide_activity_cleanup', function () use ( $wpide_log_table, $wpide_log_days ) {
    global $wpdb;
    if ( '1' === get_option( 'wpide_activity_db' ) ) {
        $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpide_log_table() . ' WHERE created < %s LIMIT 10000', gmdate( 'Y-m-d H:i:s', time() - $wpide_log_days * DAY_IN_SECONDS ) ) );
    }
} );

add_action( 'admin_menu', function () use ( $wpide_log_table ) {
    add_management_page( 'Activity Log', 'Activity Log', 'manage_options', 'wpide-activity-table', function () use ( $wpide_log_table ) {
        global $wpdb;

        $per    = 50;
        $paged  = max( 1, isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1 );
        $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
        $rows   = array();
        $total  = 0;

        if ( '1' === get_option( 'wpide_activity_db' ) ) {
            $where = '1=1';
            $args  = array();
            if ( '' !== $search ) {
                $like  = '%' . $wpdb->esc_like( $search ) . '%';
                $where = '(user_login LIKE %s OR action LIKE %s OR object LIKE %s OR details LIKE %s OR ip LIKE %s)';
                $args  = array( $like, $like, $like, $like, $like );
            }
            $table = $wpide_log_table();
            $total = (int) $wpdb->get_var( $args ? $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE $where", $args ) : "SELECT COUNT(*) FROM $table" );
            $rows  = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table WHERE $where ORDER BY id DESC LIMIT %d OFFSET %d", array_merge( $args, array( $per, ( $paged - 1 ) * $per ) ) ) );
        }

        echo '<div class="wrap"><h1>Activity Log</h1><form method="get"><input type="hidden" name="page" value="wpide-activity-table">';
        echo '<p class="search-box"><label class="screen-reader-text" for="wpide-log-search">Search the log</label><input type="search" id="wpide-log-search" name="s" value="' . esc_attr( $search ) . '"> <button class="button">Search</button></p></form>';
        echo '<table class="widefat striped"><thead><tr><th>Time (UTC)</th><th>User</th><th>Action</th><th>Object</th><th>Details</th><th>IP</th></tr></thead><tbody>';
        if ( ! $rows ) {
            echo '<tr><td colspan="6">No entries.</td></tr>';
        }
        foreach ( $rows as $row ) {
            printf( '<tr><td>%s</td><td>%s</td><td><code>%s</code></td><td>%s</td><td>%s</td><td>%s</td></tr>', esc_html( $row->created ), esc_html( $row->user_login ? $row->user_login : '-' ), esc_html( $row->action ), esc_html( $row->object ), esc_html( wp_html_excerpt( $row->details, 120, '...' ) ), esc_html( $row->ip ) );
        }
        echo '</tbody></table>';
        $links = paginate_links( array( 'base' => add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => max( 1, (int) ceil( $total / $per ) ) ) );
        if ( $links ) {
            echo '<div class="tablenav"><div class="tablenav-pages">' . wp_kses_post( $links ) . '</div></div>';
        }
        echo '</div>';
    } );
} );

Notes

Records are kept for $wpide_log_days days; the table is created the first time an administrator opens the dashboard.

How to add this snippet

  1. Install WPIDE on your WordPress site (it is free) and open Code Snippets.
  2. Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
  3. It is added inactive: review the code and settings, then switch it on.

Have a snippet worth sharing?

Publish it to the library from WPIDE or right here. Sign in with any WordPress site that runs WPIDE, free or Pro. Public snippets are reviewed by the WPIDE team before they appear.

Which site should get this snippet?

Enter the address of your WordPress site. WPIDE opens there in a new tab with the snippet ready to review and save.

Remembered in this browser only, so you are asked once.

Report this snippet

Tell the WPIDE team what is wrong: broken code, something harmful, spam or copied work.