activation( [$this, 'bootstrapping'] );
$this->activation( [$this, 'send_activation_email'] );
$this->action( 'admin_post_admin_safety_guard_deactivate', [$this, 'handle_deactivate'] );
// Safety net: apply schema changes that ship via a plugin update, where the
// activation hook does not re-fire. Idempotent and version-gated. This is
// also what provisions the tables on each site of a multisite network,
// since options (including tpsa_version) are per-site.
$this->action( 'plugins_loaded', [$this, 'maybe_upgrade_database'] );
// Multisite: give a brand-new site its tables at creation time rather
// than on whichever request happens to hit it first.
$this->action( 'wp_initialize_site', [$this, 'install_for_new_site'], 100, 1 );
}
/**
* Provision plugin tables for a newly created site on a network.
*
* @param \WP_Site|int $site New site object (or ID on older callers).
* @return void
*/
public function install_for_new_site( $site ) {
if ( !is_multisite() ) {
return;
}
// Only relevant when the plugin is active for the whole network; a
// per-site activation provisions itself on activation.
// wp_initialize_site can fire outside wp-admin (WP-CLI, REST), where
// is_plugin_active_for_network() is not loaded yet.
if ( !function_exists( 'is_plugin_active_for_network' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( !is_plugin_active_for_network( TPSA_PLUGIN_BASENAME ) ) {
return;
}
$site_id = is_object( $site ) && isset( $site->blog_id ) ? (int) $site->blog_id : (int) $site;
if ( $site_id <= 0 ) {
return;
}
switch_to_blog( $site_id );
$this->install_tables();
$this->update_db_version();
restore_current_blog();
}
/**
* Ensure the database schema is current on plugin updates.
*
* Runs the (idempotent) table install whenever the stored DB version does not
* match the plugin version, so updating the plugin without a manual reactivation
* still creates/updates the custom tables.
*
* @return void
*/
public function maybe_upgrade_database() {
if ( $this->is_database_up_to_date() ) {
return;
}
$this->install_tables();
$this->update_db_version();
}
/**
* Run installation tasks on plugin activation.
*/
public function bootstrapping() {
// Enable Limit Login Attempts by default on fresh install.
add_option( 'tpsa_limit-login-attempts_settings', [
'enable' => 1,
'max-attempts' => 3,
'block-for' => 15,
'max-lockout' => 3,
'block-message' => 'You have been locked out due to too many login attempts.',
] );
if ( !$this->is_database_up_to_date() ) {
set_transient( 'tpsm_do_activation_redirect', true, 30 );
$this->install_tables();
$this->update_db_version();
}
}
/**
* Create or update all custom plugin tables.
*
* Uses dbDelta(), which is safe to run repeatedly: it creates tables when
* missing and applies additive column changes on subsequent runs.
*
* @return void
*/
private function install_tables() {
$this->create_table(
's_logins',
"
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(100) NOT NULL,
user_agent TEXT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
login_time DATETIME NOT NULL,
login_count INT UNSIGNED NOT NULL DEFAULT 1,
is_new_ip TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY username (username),
KEY login_time (login_time)
"
);
// Every lockout check filters on ip_address, and the cleanup job filters
// on the datetime columns. Without these keys each lookup is a full
// table scan, which gets slow fast on a site under sustained attack.
$this->create_table(
'failed_logins',
"
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(100) NOT NULL,
user_agent TEXT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
first_login_time DATETIME NOT NULL,
last_login_time DATETIME NOT NULL,
login_attempts INT UNSIGNED NOT NULL DEFAULT 1,
lockouts INT UNSIGNED NOT NULL DEFAULT 0,
lockout_time DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY last_login_time (last_login_time),
KEY lockout_time (lockout_time)
"
);
// Every block the plugin performs, for the dashboard threat counter.
$this->create_table(
'threats',
"
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
threat_type VARCHAR(40) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
detail VARCHAR(191) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
request_uri VARCHAR(191) NOT NULL DEFAULT '',
blocked_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY threat_type (threat_type),
KEY ip_address (ip_address),
KEY blocked_at (blocked_at)
"
);
$this->create_table(
'block_users',
"
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_agent TEXT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
login_time DATETIME NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY login_time (login_time)
"
);
}
/**
* Email the site admin on activation, including a one-click "safe
* deactivate" link in case the plugin causes trouble.
*
* @return void
*/
public function send_activation_email() {
if ( !$this->is_database_up_to_date() ) {
return;
}
$admin_email = get_option( 'admin_email' );
if ( !is_email( $admin_email ) ) {
return;
}
$site_name = get_bloginfo( 'name' );
$site_url = home_url();
$plugin_name = 'Admin Safety Guard';
// Generate a secure token for one-click deactivate link
$token = wp_generate_password( 32, false );
update_option( 'tp_admin_safety_guard_deactivate_token', $token );
$deactivate_url = add_query_arg(
array(
'action' => 'admin_safety_guard_deactivate',
'token' => $token,
),
admin_url( 'admin-post.php' )
);
// Add nonce
$deactivate_url = wp_nonce_url( $deactivate_url, 'tpsa_safe_deactivate', 'tpsa_nonce' );
$subject = sprintf(
'[%s] %s has been activated',
$site_name,
$plugin_name
);
// Build HTML email body
$message = '
' . esc_html( $plugin_name ) . ' Activated
|
WordPress Security Notification
|
|
' . esc_html( $plugin_name ) . ' is now active on your site.
' . esc_html( $site_name ) . ' (' . esc_html( $site_url ) . ')
|
|
Hi,
' . esc_html( $plugin_name ) . ' has just been installed and activated on your website.
We're here to help keep your site more secure and stable.
|
|
If you ever feel that this plugin does not work properly, causes layout issues, or you simply want to turn it off, you have a safe fallback:
|
|
Safe Deactivate Plugin
|
If the button doesn't work, copy & paste this link into your browser (you must be logged in as an admin):
' . esc_html( $deactivate_url ) . '
|
|
Manual deactivate (alternative):
Dashboard → Plugins → Installed Plugins → "' . esc_html( $plugin_name ) . '" → Deactivate
|
|
💡 Tip: For maximum safety, always keep a recent backup of your files and database before making major changes to security settings or installing new plugins.
|
|
|
Sent from ' . esc_html( $plugin_name ) . ' • ' . esc_html( $site_name ) . '
|
|
';
$headers = array();
$from_name = $plugin_name;
$from_email = 'no-reply@' . wp_parse_url( home_url(), PHP_URL_HOST );
$headers[] = 'From: ' . $from_name . ' <' . $from_email . '>';
$headers[] = 'Content-Type: text/html; charset=UTF-8';
wp_mail( $admin_email, $subject, $message, $headers );
}
public function handle_deactivate() {
auth_redirect();
// Only allow admins with plugin activation capability
if ( !current_user_can( 'activate_plugins' ) ) {
wp_die( esc_html__( 'You do not have permission to deactivate plugins.', 'admin-safety-guard' ) );
}
// ✅ Nonce verification (fixes the warning)
$nonce = isset( $_GET['tpsa_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['tpsa_nonce'] ) ) : '';
if ( empty( $nonce ) || !wp_verify_nonce( $nonce, 'tpsa_safe_deactivate' ) ) {
wp_die( esc_html__( 'Security check failed.', 'admin-safety-guard' ) );
}
$stored_token = get_option( 'tp_admin_safety_guard_deactivate_token' );
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
// Check token
if ( empty( $stored_token ) || empty( $token ) || !hash_equals( $stored_token, $token ) ) {
wp_die( esc_html__( 'Invalid or expired deactivation link.', 'admin-safety-guard' ) );
}
// Token is valid, so delete it to avoid unlimited re-use (optional but recommended)
delete_option( 'tp_admin_safety_guard_deactivate_token' );
// Deactivate this plugin
deactivate_plugins( TPSA_PLUGIN_BASENAME );
// Redirect back to plugins page with a notice
$redirect_url = add_query_arg(
array(
'tp_secure_admin_deactivated' => 1,
),
admin_url( 'plugins.php' )
);
wp_safe_redirect( $redirect_url );
exit;
}
/**
* Create a custom database table.
*
* @param string $table_name
* @param string $table_columns
*/
private function create_table( $table_name, $table_columns ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
global $wpdb;
$full_table_name = get_tpsa_db_table_name( $table_name );
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$full_table_name} ($table_columns) {$charset_collate};";
dbDelta( $sql );
}
/**
* Check if the database is already up to date.
*
* @return bool
*/
private function is_database_up_to_date() {
$installed_ver = get_option( 'tpsa_version' );
return version_compare( $installed_ver, TPSA_PLUGIN_VERSION, '=' );
}
/**
* Update the database version stored in options.
*/
private function update_db_version() {
update_option( 'tpsa_version', TPSA_PLUGIN_VERSION );
}
}