{"id":397109,"date":"2026-11-03T14:41:32","date_gmt":"2026-11-03T14:41:32","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/novelience-password-change-alerts\/"},"modified":"2026-11-03T14:41:05","modified_gmt":"2026-11-03T14:41:05","slug":"novelience-password-change-alerts","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/novelience-password-change-alerts\/","author":23595553,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.2.3","stable_tag":"1.2.3","tested":"7.1.4","requires":"6.3","requires_php":"8.2","requires_plugins":null,"header_name":"Novelience Password Change Alerts","header_author":"Novelience","header_description":"Notifies a user after their WordPress password is changed without exposing secrets.","assets_banners_color":"","last_updated":"2026-11-03 14:41:05","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/novelience.com\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":52,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","faq","changelog"],"tags":{"1.2.3":{"tag":"1.2.3","author":"novelience","date":"2026-11-03 14:41:05","revision":3782044}},"upgrade_notice":[],"ratings":[],"assets_icons":[],"assets_banners":[],"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3782609,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"landingPage\":\"\\\/wp-admin\\\/tools.php?page=novelience-alerts-preview\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"7.1\"},\"features\":{\"networking\":false},\"steps\":[{\"step\":\"defineWpConfigConsts\",\"consts\":{\"NOVELIENCE_ALERTS_PLAYGROUND_DEMO\":true,\"WP_DEBUG\":false,\"WP_DEBUG_LOG\":false,\"WP_DEBUG_DISPLAY\":false,\"DISALLOW_FILE_EDIT\":true}},{\"step\":\"writeFile\",\"path\":\"\\\/wordpress\\\/wp-content\\\/mu-plugins\\\/novelience-alerts-preview.php\",\"data\":\"<?php\\n\\\/**\\n * Plugin Name: Novelience \\u2014 \\u00e9cran de d\\u00e9monstration Playground uniquement\\n * Description: Intercepte le courrier et expose un essai local sur un compte fictif.\\n * This helper is not part of Password Change Alerts and must not be installed on a real site.\\n *\\\/\\ndeclare(strict_types=1);\\n\\nnamespace Novelience\\\\AlertsPlaygroundDemo;\\n\\ndefined('ABSPATH') || exit;\\nif (!defined('NOVELIENCE_ALERTS_PLAYGROUND_DEMO') || NOVELIENCE_ALERTS_PLAYGROUND_DEMO !== true) {\\n    return;\\n}\\n\\n\\\/\\\/ Request memory only: no mail log, option, transient, browser storage or history.\\n$GLOBALS['novelience_alerts_preview_mail'] = [];\\n$GLOBALS['novelience_alerts_preview_sent'] = [];\\n$GLOBALS['novelience_alerts_preview_profile'] = [];\\n\\nadd_filter('pre_wp_mail', function ($shortCircuit, array $attributes): bool {\\n    $allowed = ['alerts-user@example.invalid', 'preview-admin@example.invalid'];\\n    $to = $attributes['to'] ?? [];\\n    $recipients = is_array($to) ? $to : preg_split('\\\/\\\\s*,\\\\s*\\\/', (string) $to);\\n    $safe = is_array($recipients) && $recipients !== [];\\n    foreach ((array) $recipients as $recipient) {\\n        $safe = $safe && in_array((string) $recipient, $allowed, true);\\n    }\\n    $GLOBALS['novelience_alerts_preview_mail'][] = [\\n        'recipient' => $safe ? implode(', ', $recipients) : 'Destinataire masqu\\u00e9 \\u2014 hors compte de d\\u00e9monstration',\\n        'subject' => $safe ? (string) ($attributes['subject'] ?? '') : 'Contenu masqu\\u00e9',\\n    ];\\n    \\\/\\\/ Non-null short-circuits WordPress before PHPMailer. True only simulates acceptance.\\n    return true;\\n}, PHP_INT_MAX, 2);\\n\\nfunction target_user(): ?\\\\WP_User\\n{\\n    $user = get_user_by('login', 'alerts_preview_user');\\n    if (!$user instanceof \\\\WP_User\\n        || $user->user_email !== 'alerts-user@example.invalid'\\n        || get_user_meta($user->ID, 'novelience_alerts_preview_synthetic', true) !== '1'\\n        || !in_array('subscriber', $user->roles, true)) {\\n        return null;\\n    }\\n    return $user;\\n}\\n\\n\\\/\\\/ Observe the real plugin's public action; no notification function is called by this helper.\\nadd_action('wp_password_change_notifier_sent', function ($accepted, $user, $recipient, $subject, $message): void {\\n    $target = target_user();\\n    if (!$target || !$user instanceof \\\\WP_User || (int) $user->ID !== (int) $target->ID) {\\n        return;\\n    }\\n    $safe = $recipient === 'alerts-user@example.invalid';\\n    $GLOBALS['novelience_alerts_preview_sent'][] = [\\n        'accepted_by_interceptor' => (bool) $accepted,\\n        'recipient' => $safe ? (string) $recipient : 'Destinataire modifi\\u00e9 \\u2014 contenu masqu\\u00e9',\\n        'subject' => $safe ? (string) $subject : 'Contenu masqu\\u00e9',\\n        'message' => $safe ? (string) $message : 'Contenu masqu\\u00e9',\\n        'hook_user_secrets_cleared' => $user->user_pass === '' && $user->user_activation_key === '',\\n    ];\\n}, 10, 5);\\n\\nadd_action('profile_update', function (int $id, $previous): void {\\n    $target = target_user();\\n    if (!$target || (int) $target->ID !== $id || !$previous instanceof \\\\WP_User) {\\n        return;\\n    }\\n    $GLOBALS['novelience_alerts_preview_profile'][] = [\\n        'hash_changed' => !hash_equals((string) $previous->user_pass, (string) $target->user_pass),\\n    ];\\n}, PHP_INT_MAX, 2);\\n\\nadd_action('admin_menu', function (): void {\\n    add_management_page('Essayer Password Change Alerts', 'D\\u00e9mo Alerts', 'manage_options', 'novelience-alerts-preview', __NAMESPACE__ . '\\\\\\\\render_page');\\n});\\n\\nfunction render_page(): void\\n{\\n    if (!current_user_can('manage_options')) {\\n        wp_die('Cette d\\u00e9monstration n\\u00e9cessite la session administrateur locale de Playground.');\\n    }\\n    $user = target_user();\\n    $active = function_exists('Novelience\\\\\\\\PasswordChangeAlerts\\\\\\\\profile_updated');\\n    $result = null;\\n    $mode = '';\\n    if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {\\n        check_admin_referer('novelience_alerts_preview_change');\\n        $mode = isset($_POST['novelience_preview_action']) ? sanitize_key(wp_unslash($_POST['novelience_preview_action'])) : '';\\n        if (!$user || !$active || !in_array($mode, ['change', 'unchanged'], true)) {\\n            $result = new \\\\WP_Error('preview_guard', 'Action refus\\u00e9e : v\\u00e9rifiez le plugin et le compte fictif, ou ouvrez une nouvelle instance.');\\n        } else {\\n            $data = ['ID' => $user->ID];\\n            if ($mode === 'change') {\\n                \\\/\\\/ Generated solely for the synthetic subscriber; never accepted from user input or displayed.\\n                $data['user_pass'] = wp_generate_password(32, true, true);\\n            } else {\\n                $data['display_name'] = 'Compte fictif \\u2014 d\\u00e9monstration Alerts';\\n            }\\n            $result = wp_update_user($data);\\n            unset($data);\\n        }\\n    }\\n    echo '<div class=\\\"wrap\\\"><h1>Essayer Novelience Password Change Alerts 1.2.3<\\\/h1>';\\n    echo '<div class=\\\"notice notice-info inline\\\"><p><strong>D\\u00e9mo locale, sans envoi d\\u2019e-mail.<\\\/strong> Cet \\u00e9cran est ajout\\u00e9 par un petit MU-plugin de d\\u00e9monstration ; l\\u2019extension livr\\u00e9e ne poss\\u00e8de pas cet \\u00e9cran. Aucun service de messagerie, client r\\u00e9el ou mot de passe personnel n\\u2019est utilis\\u00e9.<\\\/p><\\\/div>';\\n    echo '<p>Le bouton modifie r\\u00e9ellement le mot de passe d\\u2019un abonn\\u00e9 fictif via <code>wp_update_user()<\\\/code>. WordPress \\u00e9met <code>profile_update<\\\/code> ; le plugin officiel actif peut alors produire sa notification. Un filtre de d\\u00e9mo <code>pre_wp_mail<\\\/code> bloque tous les envois avant le transport.<\\\/p>';\\n    echo '<p><strong>Compte cibl\\u00e9 :<\\\/strong> <code>alerts_preview_user<\\\/code> \\u2014 <code>alerts-user@example.invalid<\\\/code> (fictif, r\\u00f4le abonn\\u00e9). Votre session administrateur locale reste distincte.<\\\/p>';\\n    echo '<p><strong>Plugin actif :<\\\/strong> ' . ($active ? 'oui' : 'non') . ' \\u00b7 <strong>Locale de cette requ\\u00eate :<\\\/strong> <code>' . esc_html(determine_locale()) . '<\\\/code>.<\\\/p>';\\n    echo '<p>Le texte suit la locale active de la requ\\u00eate, pas automatiquement la langue pr\\u00e9f\\u00e9r\\u00e9e du destinataire. Le catalogue fran\\u00e7ais est embarqu\\u00e9 dans le plugin ; aucun pack GlotPress officiel n\\u2019est revendiqu\\u00e9. Certains libell\\u00e9s WordPress peuvent rester anglais si son pack de langue n\\u2019est pas install\\u00e9.<\\\/p>';\\n    if (is_wp_error($result)) {\\n        echo '<div class=\\\"notice notice-error inline\\\"><p>' . esc_html($result->get_error_message()) . '<\\\/p><\\\/div>';\\n    } elseif ($result !== null) {\\n        echo '<h2>R\\u00e9sultat de cet essai<\\\/h2>';\\n        $profile = $GLOBALS['novelience_alerts_preview_profile'];\\n        $events = $GLOBALS['novelience_alerts_preview_sent'];\\n        $changed = $profile !== [] && (bool) end($profile)['hash_changed'];\\n        echo '<ul><li>\\u00c9v\\u00e9nement <code>profile_update<\\\/code> observ\\u00e9 : ' . count($profile) . '<\\\/li>';\\n        echo '<li>Empreinte du mot de passe effectivement chang\\u00e9e : ' . ($changed ? 'oui' : 'non') . '<\\\/li>';\\n        echo '<li>Action r\\u00e9elle du plugin <code>wp_password_change_notifier_sent<\\\/code> : ' . count($events) . '<\\\/li>';\\n        echo '<li>Appels <code>wp_mail<\\\/code> intercept\\u00e9s, plugin et \\u00e9ventuel courrier natif inclus : ' . count($GLOBALS['novelience_alerts_preview_mail']) . '<\\\/li><\\\/ul>';\\n        echo '<p>Attendu pour cet essai : ' . ($mode === 'change' ? 'une modification et une notification du plugin.' : 'aucune modification du mot de passe et aucune notification du plugin.') . ' Les nombres ci-dessus sont observ\\u00e9s dans cette requ\\u00eate, pas pr\\u00e9remplis.<\\\/p>';\\n        foreach ($events as $event) {\\n            echo '<section style=\\\"max-width:900px;background:white;border:1px solid #ccd0d4;padding:16px;margin:16px 0\\\"><h3>Notification r\\u00e9ellement intercept\\u00e9e<\\\/h3>';\\n            echo '<p><strong>Destinataire fictif :<\\\/strong> <code>' . esc_html($event['recipient']) . '<\\\/code><\\\/p>';\\n            echo '<p><strong>Objet :<\\\/strong> ' . esc_html($event['subject']) . '<\\\/p>';\\n            echo '<pre style=\\\"white-space:pre-wrap\\\">' . esc_html($event['message']) . '<\\\/pre>';\\n            echo '<p>Valeur de retour fournie par l\\u2019intercepteur : <code>' . ($event['accepted_by_interceptor'] ? 'true' : 'false') . '<\\\/code> \\u2014 aucune remise SMTP ni d\\u00e9livrabilit\\u00e9 prouv\\u00e9e.<\\\/p>';\\n            echo '<p>Empreinte et cl\\u00e9 de r\\u00e9initialisation vid\\u00e9es dans l\\u2019argument utilisateur du hook : ' . ($event['hook_user_secrets_cleared'] ? 'oui' : 'non') . '.<\\\/p><\\\/section>';\\n        }\\n        if ($GLOBALS['novelience_alerts_preview_mail'] !== []) {\\n            echo '<details><summary>Autres appels de courrier intercept\\u00e9s dans cet essai<\\\/summary><ul>';\\n            foreach ($GLOBALS['novelience_alerts_preview_mail'] as $mail) {\\n                echo '<li>' . esc_html($mail['recipient']) . ' \\u2014 ' . esc_html($mail['subject']) . '<\\\/li>';\\n            }\\n            echo '<\\\/ul><\\\/details>';\\n        }\\n    }\\n    echo '<h2>\\u00c0 vous d\\u2019essayer<\\\/h2><form method=\\\"post\\\">';\\n    wp_nonce_field('novelience_alerts_preview_change');\\n    echo '<p><button class=\\\"button button-primary\\\" type=\\\"submit\\\" name=\\\"novelience_preview_action\\\" value=\\\"change\\\">G\\u00e9n\\u00e9rer et changer le mot de passe fictif<\\\/button><\\\/p>';\\n    echo '<p><button class=\\\"button\\\" type=\\\"submit\\\" name=\\\"novelience_preview_action\\\" value=\\\"unchanged\\\">Enregistrer le profil sans changer le mot de passe<\\\/button><\\\/p><\\\/form>';\\n    echo '<h2>Limites de cette d\\u00e9monstration<\\\/h2><p>Les preuves et le courrier restent en m\\u00e9moire de cette requ\\u00eate PHP et disparaissent au prochain chargement. Le compte fictif conserve son \\u00e9tat dans cette instance Playground ; ouvrez une nouvelle instance pour repartir de z\\u00e9ro. Aucun historique d\\u2019e-mails n\\u2019est \\u00e9crit.<\\\/p>';\\n    echo '<p>Le plugin conserve sa configuration et sa d\\u00e9duplication par d\\u00e9faut. Cette d\\u00e9mo ne teste pas les r\\u00e9initialisations, toutes les versions WordPress\\\/PHP, un cache persistant, l\\u2019envoi externe ou la r\\u00e9ception en bo\\u00eete mail. Les notifications natives de WordPress ne sont pas d\\u00e9sactiv\\u00e9es ; elles sont intercept\\u00e9es aussi.<\\\/p>';\\n    echo '<p>Ne saisissez aucune donn\\u00e9e r\\u00e9elle et ne r\\u00e9utilisez pas ce MU-plugin sur un site de production. Developed and maintained by Novelience.<\\\/p><\\\/div>';\\n}\\n\"},{\"step\":\"setSiteOptions\",\"options\":{\"blogname\":\"Novelience \\u2014 d\\u00e9monstration locale Alerts\",\"blogdescription\":\"Compte fictif ; tous les courriers sont intercept\\u00e9s dans Playground.\",\"admin_email\":\"preview-admin@example.invalid\",\"WPLANG\":\"fr_FR\"}},{\"step\":\"installPlugin\",\"options\":{\"activate\":true},\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"novelience-password-change-alerts\"}},{\"step\":\"runPHP\",\"code\":\"<?php\\n\\\/** Blueprint setup only: check the pinned distribution and create synthetic data. *\\\/\\ndeclare(strict_types=1);\\nrequire '\\\/wordpress\\\/wp-load.php';\\n\\nif (!defined('NOVELIENCE_ALERTS_PLAYGROUND_DEMO') || NOVELIENCE_ALERTS_PLAYGROUND_DEMO !== true) {\\n    throw new RuntimeException('Missing Playground-only demo guard.');\\n}\\n$base = WP_PLUGIN_DIR . '\\\/novelience-password-change-alerts\\\/';\\n$expected = [\\n    'LICENSE' => '8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643',\\n    'languages\\\/novelience-password-change-alerts-fr_FR.mo' => 'e91fd1ae0116c358f1ede56e37a24f82917595b90645c029bac53384fb930069',\\n    'languages\\\/novelience-password-change-alerts-fr_FR.po' => '11ec1b26d3bbf02e3335eb058bf61bfc64f0a0ac829b9915d31448269a0fd3d7',\\n    'novelience-password-change-alerts.php' => '139df908a7a1e2b822b9d03c8e563d1646741c081216793634a60a0d0fc0c2cf',\\n    'product.json' => '5793a9b6f2fb2be3cc4d008bfc892f9215d4640660fa762a1fabbe535ec160ae',\\n    'readme.txt' => '346b96fae358045277b36fb96c5ee3b0b74d8effe5f61cb664af1d229f8d7d94',\\n];\\nforeach ($expected as $relative => $sha256) {\\n    if (!is_file($base . $relative) || hash_file('sha256', $base . $relative) !== $sha256) {\\n        throw new RuntimeException('Pinned Alerts 1.2.3 file check failed: ' . $relative);\\n    }\\n}\\nif (!has_filter('pre_wp_mail')) {\\n    throw new RuntimeException('Mail interception must be installed before synthetic setup.');\\n}\\n$login = 'alerts_preview_user';\\nif (username_exists($login)) {\\n    throw new RuntimeException('Use a fresh Playground instance for this demonstration.');\\n}\\n$id = wp_insert_user([\\n    'user_login' => $login,\\n    'user_pass' => wp_generate_password(32, true, true),\\n    'user_email' => 'alerts-user@example.invalid',\\n    'display_name' => 'Compte fictif \\u2014 d\\u00e9monstration Alerts',\\n    'role' => 'subscriber',\\n]);\\nif (is_wp_error($id)) {\\n    throw new RuntimeException('Unable to create the synthetic subscriber.');\\n}\\nupdate_user_meta($id, 'novelience_alerts_preview_synthetic', '1');\\n$admin = get_user_by('login', 'admin');\\nif (!$admin instanceof WP_User) {\\n    throw new RuntimeException('Expected the standard Playground admin.');\\n}\\n\\\/\\\/ The native Playground administrator can have an en_US preference independent of WPLANG.\\nupdate_user_meta($admin->ID, 'locale', 'fr_FR');\\nif (get_user_meta($admin->ID, 'locale', true) !== 'fr_FR') {\\n    throw new RuntimeException('Unable to set the synthetic administrator locale.');\\n}\\n$adminResult = wp_update_user(['ID' => $admin->ID, 'user_email' => 'preview-admin@example.invalid']);\\nif (is_wp_error($adminResult)) {\\n    throw new RuntimeException('Unable to set the synthetic administrator address.');\\n}\\nunset($id, $admin, $adminResult, $expected);\\necho 'Pinned Alerts files checked; synthetic subscriber prepared; all mail intercepted.';\\n\"},{\"step\":\"login\",\"username\":\"admin\"}]}"}},"all_blocks":[],"tagged_versions":["1.2.3"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[267,1159,1930,600],"plugin_category":[41,54],"plugin_contributors":[],"plugin_business_model":[],"class_list":["post-397109","plugin","type-plugin","status-publish","hentry","plugin_tags-email","plugin_tags-notification","plugin_tags-password","plugin_tags-security","plugin_category-communication","plugin_category-security-and-spam-protection","plugin_committers-novelience"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/novelience-password-change-alerts.svg","icon_2x":false,"generated":true},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Novelience Password Change Alerts observes <code>wp_set_password<\/code> and committed\n    profile_update events. This covers password resets, profile edits through\n    wp_update_user, and updates to existing users through <code>wp_insert_user<\/code>,\nwithout a WooCommerce dependency. Email or display-name changes alone do not\ncreate an alert, and creating a new user is not treated as a password change.<\/p>\n\n<ul>\n<li>Never logs, stores, or includes the password in an email.<\/li>\n<li>Deduplicates accidental repeated notifications.<\/li>\n<li>Lets developers filter eligibility, recipient, subject, body, headers, and\nthe deduplication interval.<\/li>\n<li>Fires an action with the final mail result for observability integrations.<\/li>\n<li>Gives each plugin filter and action a separate user-data copy with password\nhashes and password-reset keys cleared.<\/li>\n<li>Includes French translations for the plugin notice and password-change email;\nEnglish remains the source language.<\/li>\n<\/ul>\n\n<p>The extension does not disable WordPress's own notices. WordPress may also send\nits standard email for the same update. Changes made directly in SQL or in a\ncustom password store that bypasses these APIs are outside this workflow.<\/p>\n\n<p>Developed and maintained by Novelience.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20the%20email%20include%20a%20sign-in%20or%20reset%20link%3F\"><h3>Does the email include a sign-in or reset link?<\/h3><\/dt>\n<dd><p>No. This avoids placing an account-access link in the same alert channel.<\/p><\/dd>\n<dt id=\"can%20a%20project%20customize%20the%20message%3F\"><h3>Can a project customize the message?<\/h3><\/dt>\n<dd><p>Yes. Every mail field is filterable, and the\n    wp_password_change_notifier_sent action receives the result and final values.<\/p><\/dd>\n<dt id=\"how%20are%20duplicate%20events%20handled%3F\"><h3>How are duplicate events handled?<\/h3><\/dt>\n<dd><p>The existing <code>wp_password_change_notifier_*<\/code> filters and cache group remain\nunchanged. The default cache window is 30 seconds; a persistent object cache\ncan retain it across requests. Setting the window to zero disables that cache\nsuppression, while one core mutation still generates only one plugin alert.\nWhen a direct-setter event provides the previous user object, rewriting the\nsame effective credential or rehashing it is ignored. This also covers\nWordPress-native hashes that normalize leading or trailing whitespace before\nstorage. Older direct-setter events do not expose the previous hash, so an\nexplicit setter or legacy hash upgrade can still be reported.<\/p><\/dd>\n<dt id=\"how%20do%20i%20migrate%20an%20older%20private%20password%20change%20alerts%20installation%3F\"><h3>How do I migrate an older private Password Change Alerts installation?<\/h3><\/dt>\n<dd><p>Deactivate the old <code>password-change-alerts<\/code> copy before activating\n    novelience-password-change-alerts. There is no plugin-owned stored data to\nmigrate or delete. The PHP namespace prevents a function collision if the\ncopies overlap; the new copy removes only the known old password callback for\nthat request and asks an administrator to deactivate the old plugin. Existing\nfilter names and the integer-only deduplication cache remain compatible.<\/p>\n\n<p>The directory slug, folder, main file, and text domain all use\nnovelience-password-change-alerts.<\/p><\/dd>\n<dt id=\"which%20language%20is%20used%20for%20the%20alert%20email%3F\"><h3>Which language is used for the alert email?<\/h3><\/dt>\n<dd><p>The message uses the WordPress locale active when the password-change hook runs.\nFor example, an administrator's language can govern an admin request, while a\nfront-end reset normally uses the site's language. This plugin does not switch\nto each recipient's preferred language. If French is the active locale,\nthe bundled French catalog is used without a remote language pack.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.2.3<\/h4>\n\n<ul>\n<li>Removed the <code>load_plugin_textdomain()<\/code> call flagged in review.<\/li>\n<li>Registered the bundled French catalog path only for older WordPress versions and affected network activations; Core still loads translations on demand.<\/li>\n<li>Kept WordPress.org language packs ahead of the bundled catalog and retained the 6.3 minimum, existing notification behavior, hooks, and data.<\/li>\n<\/ul>\n\n<h4>1.2.2<\/h4>\n\n<ul>\n<li>Added bundled French translations for the mail subject, body, and migration notice, plus plugin metadata.<\/li>\n<li>Registered the catalog on <code>init<\/code> so translation loading does not run too early on current WordPress.<\/li>\n<li>Clarified Novelience attribution without listing an individual contact address.<\/li>\n<li>Kept the existing hooks, filters, notification rules, and data handling unchanged.<\/li>\n<\/ul>\n\n<h4>1.2.1<\/h4>\n\n<ul>\n<li>Aligned the release version with the Novelience catalogue.<\/li>\n<li>Prevented a duplicate alert when a recent WordPress core rewrites the same effective password after normalizing boundary whitespace in its native hash.<\/li>\n<li>Preserved an alert when replacing an older raw MD5 hash changes the effective credential.<\/li>\n<li>Kept existing hooks, filters, and stored data unchanged; no plugin-owned schema or settings migration is required.<\/li>\n<\/ul>\n\n<h4>0.3.1<\/h4>\n\n<ul>\n<li>Added committed profile\/API updates on WordPress 6.3 and newer, without duplicate alerts from overlapping core hooks.<\/li>\n<li>Excluded account creation, unchanged profile fields, and known same-secret setter events.<\/li>\n<li>Cleared password and reset data from separate copies passed to plugin filters and actions.<\/li>\n<li>Added the Novelience name, namespace, directory identity, and a private-installation migration path; existing public filters and cache names remain unchanged.<\/li>\n<\/ul>\n\n<h4>0.3.0<\/h4>\n\n<ul>\n<li>Added <code>wp_set_password<\/code> handling; profile\/API updates on older cores were not yet covered.<\/li>\n<li>Added mail-field filters and delivery-result action.<\/li>\n<li>Removed site-specific wording and completed the English readme.<\/li>\n<\/ul>\n\n<h4>0.2.0<\/h4>\n\n<ul>\n<li>Initial password reset notification.<\/li>\n<\/ul>","raw_excerpt":"Alerts users to password updates made through WordPress core APIs, including profile updates, resets, and direct setters.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/397109","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=397109"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/novelience"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=397109"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=397109"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=397109"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=397109"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=397109"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=397109"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}