wp-m365-login/uninstall.php
Friederich Loheide 850f0dcd54
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Fix the findings of a full second security audit
Four-part audit (OIDC/JWT/crypto, user sync, admin UI, login bypasses)
with dynamic PoCs against a real WordPress install; every fix is covered
by a regression test. Report: docs/security-audit.md, section 6.

Critical/High
- Multisite: settings, AJAX actions and certificate download require
  manage_network_options (site admins could sign in as super admin).
- Privileged accounts are only linked (sync and first sign-in) via a
  matching UPN of a member account, never via the settable mail
  attribute; the sync never changes their e-mail address; e-mail change
  notifications stay on.
- Button-only mode exempts by credential (application passwords, WP-CLI)
  instead of request context, closing bypasses through xmlrpc.php and
  REST login handlers; API requests never receive login cookies.
- Multi-tenant mode refuses guest/external identities.

Medium/Low
- Same message for right and wrong passwords; button-only no longer
  switches off when the connection breaks; server-side fallback cookie
  expiry; correct fallback key beats IP lockouts; right-most proxy hop;
  higher start limit; one object ID per account.
- Deactivation sets a random password, revokes application passwords and
  removes the role (restored on reactivation); disabled people are
  deactivated even when their mail vanished; duplicate bindings handled.
- Sync: abort on empty directory answer, no deprovisioning right after a
  tenant change, atomic run lock, strict photo path validation.
- Certificates: key bundles refused, clean re-exported certificate.
- Array-safe sanitising, encoded redirect_to, per-action nonces, escaped
  role lists, no Graph sleeps during sign-in, warnings for public groups,
  multi-tenant group rules and missing salts, uninstall clears the token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 17:10:30 +00:00

69 lines
2.4 KiB
PHP

<?php
/**
* Removes all plugin data on uninstall.
*
* @package M365_Login
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
/**
* Deletes options, transients and user meta for one site.
*/
function m365_login_uninstall_site() {
global $wpdb;
// Cached Graph app token (may live in a persistent object cache instead of the options table).
$settings = get_option( 'm365_login_settings', array() );
if ( is_array( $settings ) && ! empty( $settings['client_id'] ) ) {
$tenant = ! empty( $settings['tenant_id'] ) ? $settings['tenant_id'] : 'organizations';
foreach ( array( 'secret', 'certificate' ) as $method ) {
delete_transient( 'm365_login_apptoken_' . md5( $tenant . '|' . $settings['client_id'] . '|' . $method ) );
}
}
delete_option( 'm365_login_settings' );
delete_option( 'm365_login_sync_lock' );
delete_option( 'm365_login_sync_tenant' );
delete_option( 'm365_login_sync_report' );
wp_clear_scheduled_hook( 'm365_login_sync' );
// Synced profile photos (uploads/m365-login-avatars/).
$uploads = wp_get_upload_dir();
$dir = trailingslashit( $uploads['basedir'] ) . 'm365-login-avatars';
if ( is_dir( $dir ) ) {
foreach ( (array) glob( $dir . '/m365-*' ) as $file ) {
wp_delete_file( $file );
}
@rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- best effort, may contain foreign files.
}
// Transients: state records and JWKS cache.
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
$wpdb->esc_like( '_transient_m365_login_' ) . '%',
$wpdb->esc_like( '_transient_timeout_m365_login_' ) . '%'
)
);
}
if ( is_multisite() ) {
$m365_login_site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
foreach ( $m365_login_site_ids as $m365_login_site_id ) {
switch_to_blog( $m365_login_site_id );
m365_login_uninstall_site();
restore_current_blog();
}
} else {
m365_login_uninstall_site();
}
// User meta is global. Imported accounts stay; copied profile fields (m365_*) are kept as ordinary user data.
foreach ( array( '_m365_login_oid', '_m365_login_last_login', '_m365_login_synced', '_m365_login_disabled', '_m365_login_last_sync', '_m365_login_photo' ) as $m365_login_meta_key ) {
delete_metadata( 'user', 0, $m365_login_meta_key, '', true );
}