Plugin Directory

Changeset 3750420


Ignore:
Timestamp:
10/16/2026 12:39:58 PM (6 days ago)
Author:
devdome
Message:

1.0.10: Analytics note, shared library 1.7.13, uninstall removes the update cache [Fable 5]

Location:
devdome-admin-cleaner
Files:
2 added
18 edited
1 copied

Legend:

Unmodified
Added
Removed
  • devdome-admin-cleaner/tags/1.0.10/devdome-admin-cleaner.php

    r3721976 r3750420  
    44Plugin URI: https://devdome.com/
    55Description: Clean up your WordPress admin in one click. Hide promo notices, remove dashboard clutter, simplify the admin bar, and give client roles a simpler workspace. Part of the DevDome suite.
    6 Version: 1.0.9
     6Version: 1.0.10
    77Author: DevDome
    88Author URI: https://devdome.com
    … …  
    2424}
    2525
    26 define('DEVDADCL_VERSION', '1.0.9');
     26define('DEVDADCL_VERSION', '1.0.10');
    2727define('DEVDADCL_DIR', plugin_dir_path(__FILE__));
    2828define('DEVDADCL_URL', plugin_dir_url(__FILE__));
  • devdome-admin-cleaner/tags/1.0.10/includes/admin.php

    r3721976 r3750420  
    367367    ?>
    368368    <div class="dd-app min-h-screen bg-gray-50 text-[#3c434a] font-sans text-[13px]">
     369        <?php if (function_exists('devdcorev1_analytics_card')) { devdcorev1_analytics_card(); } ?>
    369370        <?php if ('' !== $err_text) : ?>
    370371            <div class="dd-banner" data-ac-flash="1" style="margin:16px 24px 0;border-color:#fecaca;background:#fef2f2;color:#991b1b;"><?php echo esc_html($err_text); ?><?php if (function_exists('devdcorev1_error_report_button')) { echo devdcorev1_error_report_button('devdome-admin-cleaner', DEVDADCL_VERSION, ('db' === $err ? 'Database error during an Admin Cleaner action: ' : 'Settings save did not land: ') . $err_text, 'Admin Cleaner ' . $tab); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by the core helper ?></div>
  • devdome-admin-cleaner/tags/1.0.10/lib/devdome-core/devdome-core.php

    r3721976 r3750420  
    2121}
    2222define('DEVDCOREV1_LOADED', true);
    23 define('DEVDCOREV1_VERSION', '1.7.10');
     23define('DEVDCOREV1_VERSION', '1.7.13');
    2424
    2525if (!defined('DEVDCOREV1_FEED_ENDPOINT')) {
    … …  
    295295    add_action('rest_api_init', 'devdcorev1_beacon_rest_init');
    296296
     297    /**
     298     * Cloudflare edge ranges, copied from https://www.cloudflare.com/ips-v4/ and /ips-v6/ on 2026-10-02
     299     * (core 1.7.12). Shipped, never fetched at runtime; refreshed with core releases.
     300     */
     301    function devdcorev1_cloudflare_cidrs()
     302    {
     303        return array(
     304            '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18',
     305            '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17',
     306            '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
     307            '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32',
     308            '2a06:98c0::/29', '2c0f:f248::/32',
     309        );
     310    }
     311
     312    /** True only when the TCP peer (REMOTE_ADDR) is a Cloudflare edge. Cached per request. */
     313    function devdcorev1_peer_is_cloudflare()
     314    {
     315        static $cached = null;
     316        if ($cached !== null) {
     317            return $cached;
     318        }
     319        $peer = isset($_SERVER['REMOTE_ADDR']) ? filter_var(trim(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))), FILTER_VALIDATE_IP) : false;
     320        if (false === $peer) {
     321            return $cached = false;
     322        }
     323        $idx = devdcorev1_build_cidr_index(devdcorev1_cloudflare_cidrs());
     324        return $cached = devdcorev1_ip_in_cidr_index($peer, $idx['v4'], $idx['v6']);
     325    }
     326
    297327    /** Client IP for the beacon (Cloudflare-aware). */
    298328    function devdcorev1_request_ip()
    299329    {
    300         // CF-Connecting-IP is believed only when a plugin vouches that the peer is Cloudflare (the bot-protection
    301         // plugin checks REMOTE_ADDR against the published ranges); otherwise anyone could name a victim address.
    302         $trust_cf = (bool) apply_filters('devdcorev1_trust_cf_ip', false);
     330        // CF-Connecting-IP is believed only when the peer really is Cloudflare: the core checks REMOTE_ADDR against the
     331        // shipped edge ranges (core 1.7.12, DeepSeek core round 1), and a plugin may still vouch through the filter.
     332        // Otherwise anyone could name a victim address by sending the header themselves.
     333        $trust_cf = (bool) apply_filters('devdcorev1_trust_cf_ip', devdcorev1_peer_is_cloudflare());
    303334        foreach ($trust_cf ? array('HTTP_CF_CONNECTING_IP', 'REMOTE_ADDR') : array('REMOTE_ADDR') as $h) {
    304335            if (!empty($_SERVER[$h])) {
    … …  
    314345    }
    315346
     347    /** Delete this guard's expired transient pairs (the value row and its timeout row), nothing else. */
     348    function devdcorev1_beacon_rate_sweep()
     349    {
     350        global $wpdb;
     351        $like = $wpdb->esc_like('_transient_timeout_devdcorev1_beacon_rate_') . '%';
     352        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- scoped sweep of this library's own expired transients, the way core's delete_expired_transients() works.
     353        $wpdb->query($wpdb->prepare(
     354            "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
     355             WHERE a.option_name LIKE %s
     356               AND b.option_name = CONCAT('_transient_', SUBSTRING(a.option_name, 20))
     357               AND a.option_value < %d",
     358            $like,
     359            time()
     360        ));
     361    }
     362
    316363    /**
    317364     * Receive one beacon: parse + classify, then re-broadcast to listeners via the
    … …  
    334381        // FILTER_VALIDATE_IP sanitizes the transport peer; a non-IP value (impossible from a real
    335382        // TCP peer) collapses into the shared '' bucket, which is still rate-limited.
    336         $bucket_ip = (defined('DEVDCOREV1_TRUST_CF_IP') && DEVDCOREV1_TRUST_CF_IP)
     383        // Core audit 2026-10-16: since 1.7.12 devdcorev1_request_ip() returns the CF-Connecting-IP header ONLY
     384        // when the TCP peer is a verified Cloudflare edge (shipped ranges) and REMOTE_ADDR otherwise, so $ip can
     385        // no longer be minted by a rotated header. Bucket on it: before this, every visitor behind the same
     386        // Cloudflare edge shared one 120/min bucket and a busy site dropped real beacons as a flood.
     387        $bucket_ip = $ip !== ''
    337388            ? $ip
    338389            : (string) filter_var(isset($_SERVER['REMOTE_ADDR']) ? trim(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))) : '', FILTER_VALIDATE_IP);
    339         $key = 'devdcorev1_beacon_rate_' . md5($bucket_ip) . '_' . (int) floor(time() / 60);
    340         $n = (int) get_transient($key);
    341         if ($n >= 120) {
     390        // One bucket per IP that resets every minute, not one per IP per minute (core audit 2026-10-16):
     391        // the old key carried the minute, so without an object cache every IP minted two fresh wp_options
     392        // rows a minute that sat expired until WordPress's daily sweep (half of all beacon rows on a fleet
     393        // site). Now a returning IP reuses its row, and about one request in fifty removes the expired ones.
     394        $key = 'devdcorev1_beacon_rate_' . md5($bucket_ip);
     395        $minute = (int) floor(time() / 60);
     396        $b = get_transient($key);
     397        if (!is_array($b) || !isset($b['m'], $b['n']) || (int) $b['m'] !== $minute) {
     398            $b = array('m' => $minute, 'n' => 0);
     399        }
     400        if ((int) $b['n'] >= 120) {
    342401            return new WP_REST_Response(array('ok' => true), 202); // accepted, dropped
    343402        }
    344         set_transient($key, $n + 1, 120);
     403        $b['n'] = (int) $b['n'] + 1;
     404        set_transient($key, $b, 120);
     405        if (!wp_using_ext_object_cache() && 1 === wp_rand(1, 50)) {
     406            devdcorev1_beacon_rate_sweep();
     407        }
    345408
    346409        $flag = static function ($k) use ($req) { return $req->get_param($k) ? 1 : 0; };
    … …  
    513576    if (file_exists(__DIR__ . '/hub-install-selfhost.php')) {
    514577        require_once __DIR__ . '/hub-install-selfhost.php';
     578    require_once __DIR__ . '/hub-telemetry.php'; // opt-in "Help improve DevDome" usage reports, core 1.7.12
    515579    }
    516580}
  • devdome-admin-cleaner/tags/1.0.10/lib/devdome-core/hub-account.php

    r3704110 r3750420  
    1515 * identity options are routed to the main site through option filters, which covers every
    1616 * plugin's own get_option() calls without touching them. Subdomain multisite blogs keep their own
    17  * host and their own identity. Connection state stays per blog: each blog presses Connect once
    18  * and the server treats it as the same site. Core 1.7.0.
     17 * host and their own identity, and so does a subdirectory blog mapped to its own domain (core 1.7.11).
     18 * Connection state stays per blog: each blog presses Connect once and the server treats it as the
     19 * same site. Core 1.7.0.
    1920 */
    2021if (!function_exists('devdcorev1_shared_identity')) {
    2122    function devdcorev1_shared_identity()
    2223    {
    23         // Three cheap calls, evaluated each time: switch_to_blog() can change the answer within one request.
    24         return function_exists('is_multisite') && is_multisite()
     24        // Cheap calls, evaluated each time: switch_to_blog() can change the answer within one request.
     25        if (!(function_exists('is_multisite') && is_multisite()
    2526            && function_exists('is_subdomain_install') && !is_subdomain_install()
    26             && function_exists('is_main_site') && !is_main_site();
     27            && function_exists('is_main_site') && !is_main_site())) {
     28            return false;
     29        }
     30        // Core 1.7.11: identity = the DOMAIN, so a subsite MAPPED to its own domain is its own site. It shares
     31        // the network's token + Site ID only while its host is still the network's host (Codex, Analytics 1.1.5).
     32        $blog_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
     33        $net_host  = function_exists('network_home_url') ? strtolower((string) wp_parse_url(network_home_url('/'), PHP_URL_HOST)) : '';
     34        return '' !== $blog_host && $blog_host === $net_host;
    2735    }
    2836    /** pre_option_<identity option>: the main site's value, read once per request. */
  • devdome-admin-cleaner/tags/1.0.10/lib/devdome-core/hub.php

    r3721976 r3750420  
    454454
    455455/** Core's registry-driven Overview (the default v1 hub). */
     456/* ------------------------- Analytics mention card (core 1.7.13) ------------------------- */
     457// One dismissible card on a plugin's OWN settings page pointing at the free DevDome Analytics
     458// plugin on WordPress.org (a plugin mention, not an upsell: guideline 11 allows linking our own
     459// products on our own screens). Links only to wp-admin's plugin-install screen; never installs
     460// or activates anything itself. Hidden for good once dismissed. Plugins call
     461// devdcorev1_analytics_card() at the top of their settings page.
     462function devdcorev1_analytics_card_dismiss()
     463{
     464    if (empty($_GET['devdcorev1_an_card_dismiss']) || !current_user_can('manage_options')) {
     465        return;
     466    }
     467    check_admin_referer('devdcorev1_an_card');
     468    update_option('devdcorev1_an_card_dismissed', 1, false);
     469    wp_safe_redirect(remove_query_arg(array('devdcorev1_an_card_dismiss', '_wpnonce')));
     470    exit;
     471}
     472add_action('admin_init', 'devdcorev1_analytics_card_dismiss');
     473
     474function devdcorev1_analytics_card()
     475{
     476    if (!current_user_can('manage_options') || get_option('devdcorev1_an_card_dismissed', 0)) {
     477        return;
     478    }
     479    // Read from the options, not is_plugin_active(): that helper lives in the admin-only plugin helpers file, which the
     480    // bundled library never loads itself (wp.org review rule, kept by the suite tests).
     481    $an_file = 'devdome-analytics/devdome-analytics.php';
     482    $active  = in_array($an_file, (array) get_option('active_plugins', array()), true)
     483        || (is_multisite() && array_key_exists($an_file, (array) get_site_option('active_sitewide_plugins', array())));
     484    if ($active) {
     485        $btn_url = add_query_arg(array('page' => 'devdome-analytics'), admin_url('admin.php'));
     486        $btn_txt = 'Open Analytics';
     487    } elseif (current_user_can('install_plugins')) {
     488        // The search results page, not tab=plugin-information: that page is the popup's iframe body and
     489        // opened directly it has no admin menu, no admin bar and no way back (core audit 2026-10-16).
     490        $btn_url = add_query_arg(array('s' => 'devdome-analytics', 'tab' => 'search', 'type' => 'term'), admin_url('plugin-install.php'));
     491        $btn_txt = 'Install Analytics';
     492    } else {
     493        $btn_url = 'https://wordpress.org/plugins/devdome-analytics/';
     494        $btn_txt = 'View On WordPress.org';
     495    }
     496    $dismiss = wp_nonce_url(add_query_arg('devdcorev1_an_card_dismiss', 1), 'devdcorev1_an_card');
     497    $tools   = add_query_arg(array('page' => DEVDCOREV1_TOOLS_MENU_SLUG), admin_url('admin.php'));
     498    ?>
     499    <style>
     500    .ddan-card{display:flex;align-items:center;gap:16px;margin:16px 24px 0;padding:14px 18px;background:#0b1224;color:#fff;border-radius:12px;position:relative;max-width:1000px;box-sizing:border-box}
     501    .ddan-ic{flex:0 0 42px;width:42px;height:42px;border-radius:10px;background:#4f46e5;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:16px;letter-spacing:-.5px}
     502    .ddan-body{flex:1 1 auto;min-width:0}
     503    .ddan-body strong{display:block;font-size:15px;line-height:1.3;margin:0 0 3px;color:#fff;font-weight:800}
     504    .ddan-body span{display:block;font-size:13px;line-height:1.45;color:#fff;font-weight:600}
     505    .ddan-body .ddan-key{margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.18)}
     506    .ddan-body a{color:#c7d2fe;text-decoration:underline;font-weight:700}
     507    .ddan-body a:hover{text-decoration:underline}
     508    .ddan-btn{flex:0 0 auto;background:#4f46e5;color:#fff;font-weight:700;font-size:13px;padding:9px 16px;border-radius:9px;text-decoration:none;white-space:nowrap}
     509    .ddan-btn:hover,.ddan-btn:focus{background:#4338ca;color:#fff}
     510    .ddan-x{position:absolute;top:6px;right:10px;color:#c7d2fe;text-decoration:none;font-size:18px;line-height:1}
     511    .ddan-x:hover{color:#fff}
     512    @media (max-width:782px){.ddan-card{flex-wrap:wrap;margin:12px 10px 0}.ddan-btn{width:100%;text-align:center}}
     513    </style>
     514    <div class="ddan-card">
     515        <span class="ddan-ic">DD</span>
     516        <div class="ddan-body">
     517            <strong>See your real visitors, not the bots. Free up to 50,000 pageviews a month.</strong>
     518            <span>DevDome Analytics shows how many real people visit your site and how many hits are bots, with no cookies.</span>
     519            <span>More DevDome plugins block bad bots, remove malware, clean unused media, find broken links, manage redirects and affiliate links, tidy the admin and log what users do. <a href="<?php echo esc_url($tools); ?>">See all plugins</a></span>
     520            <span class="ddan-key">One DevDome account is the key to everything: connect it once and every plugin gets its dashboard and email alerts.</span>
     521        </div>
     522        <a class="ddan-btn" href="<?php echo esc_url($btn_url); ?>"><?php echo esc_html($btn_txt); ?></a>
     523        <a class="ddan-x" href="<?php echo esc_url($dismiss); ?>" title="Dismiss" aria-label="Dismiss">&times;</a>
     524    </div>
     525    <?php
     526}
     527
    456528function devdcorev1_hub_render_default()
    457529{
  • devdome-admin-cleaner/tags/1.0.10/lib/devdome-core/loader.php

    r3721976 r3750420  
    5353}
    5454
    55 DEVDCOREV1_Loader::register('1.7.10', __DIR__ . '/devdome-core.php');
     55DEVDCOREV1_Loader::register('1.7.13', __DIR__ . '/devdome-core.php');
  • devdome-admin-cleaner/tags/1.0.10/lib/devdome-core/uninstall.php

    r3704110 r3750420  
    4242        delete_option('devdcorev1_inventory_consent');
    4343        delete_option('devdcorev1_connection_gen');
     44        // Core audit 2026-10-16: the Analytics mention card dismissal (1.7.13), the update-heal records
     45        // (1.7.2) and the per-user "Report a bug" bubble flag (1.7.0) were never swept.
     46        delete_option('devdcorev1_an_card_dismissed');
     47        delete_option('devdcorev1_heal_leftovers');
     48        delete_option('devdcorev1_heal_untracked');
     49        delete_metadata('user', 0, 'devdcorev1_bug_hint_seen', '', true);
    4450        // The connection itself (core 1.6.1): identifiers, the site token, the cached account
    4551        // and connection state, and every short-lived connect/beacon transient.
    46         foreach (array('devdcorev1_site_id', 'devdcorev1_site_token', 'devdcorev1_account_id', 'devdcorev1_account_email', 'devdcorev1_account', 'devdcorev1_conn_state', 'devdcorev1_conn_checked', 'devdcorev1_hub_catalog') as $opt) {
     52        foreach (array('devdcorev1_telemetry', 'devdcorev1_telemetry_salt', 'devdcorev1_site_id', 'devdcorev1_site_token', 'devdcorev1_account_id', 'devdcorev1_account_email', 'devdcorev1_account', 'devdcorev1_conn_state', 'devdcorev1_conn_checked', 'devdcorev1_hub_catalog') as $opt) {
    4753            delete_option($opt);
    4854        }
  • devdome-admin-cleaner/tags/1.0.10/readme.txt

    r3721976 r3750420  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.9
     7Stable tag: 1.0.10
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    190190
    191191== Changelog ==
     192
     193= 1.0.10 =
     194
     195* A dismissible note at the top of the settings page mentions the free DevDome Analytics plugin, with a button that opens the plugin installer (or the Analytics screen when it is already active). Dismiss hides it for good.
     196* Shared DevDome library 1.7.13: uninstalling the last DevDome plugin also removes the note's dismissal, the update-repair records and the per-user "Report a bug" flag; the visitor-beacon flood guard keeps one short-lived record per address and clears its expired ones itself, and behind Cloudflare it counts per visitor instead of per Cloudflare address.
     197* Shared DevDome library 1.7.12: a Cloudflare visitor address header is trusted only when the connection itself comes from a Cloudflare address, so a forged header from anywhere else is ignored.
     198* Shared DevDome library 1.7.11: on a subdirectory multisite, a site mapped to its own domain is its own site and no longer shares the network's connection.
     199* Deleting the plugin also removes the cached update-check answer it kept in the database.
    192200
    193201= 1.0.9 =
    … …  
    272280
    273281* Initial release: one-click admin cleanup with a live clutter score, a notice inbox with snooze/mute/restore (criticals always kept visible), per-widget dashboard cleanup, admin-bar and footer/version hiding, role-based Client Mode, and five ready-made presets.
     282
     283== Upgrade Notice ==
     284
     285= 1.0.10 =
     286Housekeeping release. Nothing changes on the screen.
     287
     288= 1.0.9 =
     289Shared DevDome library 1.7.10 and WordPress.org review fixes (escaping, no output buffering). Settings are unaffected.
  • devdome-admin-cleaner/tags/1.0.10/uninstall.php

    r3704110 r3750420  
    4444// plugin still installed, so removing this one never orphans the core for the others).
    4545require_once __DIR__ . '/lib/devdome-core/uninstall.php';
     46// The suite updater caches its update manifest under a hashed key, per site and (on multisite) network wide.
     47// Clear both spellings, or the cache outlives the plugin.
     48$devdadcl_updater_cache = 'devdome_upd_' . md5('devdome-admin-cleaner');
     49delete_transient($devdadcl_updater_cache);
     50if (is_multisite()) {
     51    delete_site_transient($devdadcl_updater_cache);
     52}
    4653devdcorev1_uninstall_cleanup('devdome-admin-cleaner/devdome-admin-cleaner.php');
    4754
  • devdome-admin-cleaner/trunk/devdome-admin-cleaner.php

    r3721976 r3750420  
    44Plugin URI: https://devdome.com/
    55Description: Clean up your WordPress admin in one click. Hide promo notices, remove dashboard clutter, simplify the admin bar, and give client roles a simpler workspace. Part of the DevDome suite.
    6 Version: 1.0.9
     6Version: 1.0.10
    77Author: DevDome
    88Author URI: https://devdome.com
    … …  
    2424}
    2525
    26 define('DEVDADCL_VERSION', '1.0.9');
     26define('DEVDADCL_VERSION', '1.0.10');
    2727define('DEVDADCL_DIR', plugin_dir_path(__FILE__));
    2828define('DEVDADCL_URL', plugin_dir_url(__FILE__));
  • devdome-admin-cleaner/trunk/includes/admin.php

    r3721976 r3750420  
    367367    ?>
    368368    <div class="dd-app min-h-screen bg-gray-50 text-[#3c434a] font-sans text-[13px]">
     369        <?php if (function_exists('devdcorev1_analytics_card')) { devdcorev1_analytics_card(); } ?>
    369370        <?php if ('' !== $err_text) : ?>
    370371            <div class="dd-banner" data-ac-flash="1" style="margin:16px 24px 0;border-color:#fecaca;background:#fef2f2;color:#991b1b;"><?php echo esc_html($err_text); ?><?php if (function_exists('devdcorev1_error_report_button')) { echo devdcorev1_error_report_button('devdome-admin-cleaner', DEVDADCL_VERSION, ('db' === $err ? 'Database error during an Admin Cleaner action: ' : 'Settings save did not land: ') . $err_text, 'Admin Cleaner ' . $tab); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by the core helper ?></div>
  • devdome-admin-cleaner/trunk/lib/devdome-core/devdome-core.php

    r3721976 r3750420  
    2121}
    2222define('DEVDCOREV1_LOADED', true);
    23 define('DEVDCOREV1_VERSION', '1.7.10');
     23define('DEVDCOREV1_VERSION', '1.7.13');
    2424
    2525if (!defined('DEVDCOREV1_FEED_ENDPOINT')) {
    … …  
    295295    add_action('rest_api_init', 'devdcorev1_beacon_rest_init');
    296296
     297    /**
     298     * Cloudflare edge ranges, copied from https://www.cloudflare.com/ips-v4/ and /ips-v6/ on 2026-10-02
     299     * (core 1.7.12). Shipped, never fetched at runtime; refreshed with core releases.
     300     */
     301    function devdcorev1_cloudflare_cidrs()
     302    {
     303        return array(
     304            '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18',
     305            '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17',
     306            '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
     307            '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32',
     308            '2a06:98c0::/29', '2c0f:f248::/32',
     309        );
     310    }
     311
     312    /** True only when the TCP peer (REMOTE_ADDR) is a Cloudflare edge. Cached per request. */
     313    function devdcorev1_peer_is_cloudflare()
     314    {
     315        static $cached = null;
     316        if ($cached !== null) {
     317            return $cached;
     318        }
     319        $peer = isset($_SERVER['REMOTE_ADDR']) ? filter_var(trim(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))), FILTER_VALIDATE_IP) : false;
     320        if (false === $peer) {
     321            return $cached = false;
     322        }
     323        $idx = devdcorev1_build_cidr_index(devdcorev1_cloudflare_cidrs());
     324        return $cached = devdcorev1_ip_in_cidr_index($peer, $idx['v4'], $idx['v6']);
     325    }
     326
    297327    /** Client IP for the beacon (Cloudflare-aware). */
    298328    function devdcorev1_request_ip()
    299329    {
    300         // CF-Connecting-IP is believed only when a plugin vouches that the peer is Cloudflare (the bot-protection
    301         // plugin checks REMOTE_ADDR against the published ranges); otherwise anyone could name a victim address.
    302         $trust_cf = (bool) apply_filters('devdcorev1_trust_cf_ip', false);
     330        // CF-Connecting-IP is believed only when the peer really is Cloudflare: the core checks REMOTE_ADDR against the
     331        // shipped edge ranges (core 1.7.12, DeepSeek core round 1), and a plugin may still vouch through the filter.
     332        // Otherwise anyone could name a victim address by sending the header themselves.
     333        $trust_cf = (bool) apply_filters('devdcorev1_trust_cf_ip', devdcorev1_peer_is_cloudflare());
    303334        foreach ($trust_cf ? array('HTTP_CF_CONNECTING_IP', 'REMOTE_ADDR') : array('REMOTE_ADDR') as $h) {
    304335            if (!empty($_SERVER[$h])) {
    … …  
    314345    }
    315346
     347    /** Delete this guard's expired transient pairs (the value row and its timeout row), nothing else. */
     348    function devdcorev1_beacon_rate_sweep()
     349    {
     350        global $wpdb;
     351        $like = $wpdb->esc_like('_transient_timeout_devdcorev1_beacon_rate_') . '%';
     352        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- scoped sweep of this library's own expired transients, the way core's delete_expired_transients() works.
     353        $wpdb->query($wpdb->prepare(
     354            "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
     355             WHERE a.option_name LIKE %s
     356               AND b.option_name = CONCAT('_transient_', SUBSTRING(a.option_name, 20))
     357               AND a.option_value < %d",
     358            $like,
     359            time()
     360        ));
     361    }
     362
    316363    /**
    317364     * Receive one beacon: parse + classify, then re-broadcast to listeners via the
    … …  
    334381        // FILTER_VALIDATE_IP sanitizes the transport peer; a non-IP value (impossible from a real
    335382        // TCP peer) collapses into the shared '' bucket, which is still rate-limited.
    336         $bucket_ip = (defined('DEVDCOREV1_TRUST_CF_IP') && DEVDCOREV1_TRUST_CF_IP)
     383        // Core audit 2026-10-16: since 1.7.12 devdcorev1_request_ip() returns the CF-Connecting-IP header ONLY
     384        // when the TCP peer is a verified Cloudflare edge (shipped ranges) and REMOTE_ADDR otherwise, so $ip can
     385        // no longer be minted by a rotated header. Bucket on it: before this, every visitor behind the same
     386        // Cloudflare edge shared one 120/min bucket and a busy site dropped real beacons as a flood.
     387        $bucket_ip = $ip !== ''
    337388            ? $ip
    338389            : (string) filter_var(isset($_SERVER['REMOTE_ADDR']) ? trim(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))) : '', FILTER_VALIDATE_IP);
    339         $key = 'devdcorev1_beacon_rate_' . md5($bucket_ip) . '_' . (int) floor(time() / 60);
    340         $n = (int) get_transient($key);
    341         if ($n >= 120) {
     390        // One bucket per IP that resets every minute, not one per IP per minute (core audit 2026-10-16):
     391        // the old key carried the minute, so without an object cache every IP minted two fresh wp_options
     392        // rows a minute that sat expired until WordPress's daily sweep (half of all beacon rows on a fleet
     393        // site). Now a returning IP reuses its row, and about one request in fifty removes the expired ones.
     394        $key = 'devdcorev1_beacon_rate_' . md5($bucket_ip);
     395        $minute = (int) floor(time() / 60);
     396        $b = get_transient($key);
     397        if (!is_array($b) || !isset($b['m'], $b['n']) || (int) $b['m'] !== $minute) {
     398            $b = array('m' => $minute, 'n' => 0);
     399        }
     400        if ((int) $b['n'] >= 120) {
    342401            return new WP_REST_Response(array('ok' => true), 202); // accepted, dropped
    343402        }
    344         set_transient($key, $n + 1, 120);
     403        $b['n'] = (int) $b['n'] + 1;
     404        set_transient($key, $b, 120);
     405        if (!wp_using_ext_object_cache() && 1 === wp_rand(1, 50)) {
     406            devdcorev1_beacon_rate_sweep();
     407        }
    345408
    346409        $flag = static function ($k) use ($req) { return $req->get_param($k) ? 1 : 0; };
    … …  
    513576    if (file_exists(__DIR__ . '/hub-install-selfhost.php')) {
    514577        require_once __DIR__ . '/hub-install-selfhost.php';
     578    require_once __DIR__ . '/hub-telemetry.php'; // opt-in "Help improve DevDome" usage reports, core 1.7.12
    515579    }
    516580}
  • devdome-admin-cleaner/trunk/lib/devdome-core/hub-account.php

    r3704110 r3750420  
    1515 * identity options are routed to the main site through option filters, which covers every
    1616 * plugin's own get_option() calls without touching them. Subdomain multisite blogs keep their own
    17  * host and their own identity. Connection state stays per blog: each blog presses Connect once
    18  * and the server treats it as the same site. Core 1.7.0.
     17 * host and their own identity, and so does a subdirectory blog mapped to its own domain (core 1.7.11).
     18 * Connection state stays per blog: each blog presses Connect once and the server treats it as the
     19 * same site. Core 1.7.0.
    1920 */
    2021if (!function_exists('devdcorev1_shared_identity')) {
    2122    function devdcorev1_shared_identity()
    2223    {
    23         // Three cheap calls, evaluated each time: switch_to_blog() can change the answer within one request.
    24         return function_exists('is_multisite') && is_multisite()
     24        // Cheap calls, evaluated each time: switch_to_blog() can change the answer within one request.
     25        if (!(function_exists('is_multisite') && is_multisite()
    2526            && function_exists('is_subdomain_install') && !is_subdomain_install()
    26             && function_exists('is_main_site') && !is_main_site();
     27            && function_exists('is_main_site') && !is_main_site())) {
     28            return false;
     29        }
     30        // Core 1.7.11: identity = the DOMAIN, so a subsite MAPPED to its own domain is its own site. It shares
     31        // the network's token + Site ID only while its host is still the network's host (Codex, Analytics 1.1.5).
     32        $blog_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
     33        $net_host  = function_exists('network_home_url') ? strtolower((string) wp_parse_url(network_home_url('/'), PHP_URL_HOST)) : '';
     34        return '' !== $blog_host && $blog_host === $net_host;
    2735    }
    2836    /** pre_option_<identity option>: the main site's value, read once per request. */
  • devdome-admin-cleaner/trunk/lib/devdome-core/hub.php

    r3721976 r3750420  
    454454
    455455/** Core's registry-driven Overview (the default v1 hub). */
     456/* ------------------------- Analytics mention card (core 1.7.13) ------------------------- */
     457// One dismissible card on a plugin's OWN settings page pointing at the free DevDome Analytics
     458// plugin on WordPress.org (a plugin mention, not an upsell: guideline 11 allows linking our own
     459// products on our own screens). Links only to wp-admin's plugin-install screen; never installs
     460// or activates anything itself. Hidden for good once dismissed. Plugins call
     461// devdcorev1_analytics_card() at the top of their settings page.
     462function devdcorev1_analytics_card_dismiss()
     463{
     464    if (empty($_GET['devdcorev1_an_card_dismiss']) || !current_user_can('manage_options')) {
     465        return;
     466    }
     467    check_admin_referer('devdcorev1_an_card');
     468    update_option('devdcorev1_an_card_dismissed', 1, false);
     469    wp_safe_redirect(remove_query_arg(array('devdcorev1_an_card_dismiss', '_wpnonce')));
     470    exit;
     471}
     472add_action('admin_init', 'devdcorev1_analytics_card_dismiss');
     473
     474function devdcorev1_analytics_card()
     475{
     476    if (!current_user_can('manage_options') || get_option('devdcorev1_an_card_dismissed', 0)) {
     477        return;
     478    }
     479    // Read from the options, not is_plugin_active(): that helper lives in the admin-only plugin helpers file, which the
     480    // bundled library never loads itself (wp.org review rule, kept by the suite tests).
     481    $an_file = 'devdome-analytics/devdome-analytics.php';
     482    $active  = in_array($an_file, (array) get_option('active_plugins', array()), true)
     483        || (is_multisite() && array_key_exists($an_file, (array) get_site_option('active_sitewide_plugins', array())));
     484    if ($active) {
     485        $btn_url = add_query_arg(array('page' => 'devdome-analytics'), admin_url('admin.php'));
     486        $btn_txt = 'Open Analytics';
     487    } elseif (current_user_can('install_plugins')) {
     488        // The search results page, not tab=plugin-information: that page is the popup's iframe body and
     489        // opened directly it has no admin menu, no admin bar and no way back (core audit 2026-10-16).
     490        $btn_url = add_query_arg(array('s' => 'devdome-analytics', 'tab' => 'search', 'type' => 'term'), admin_url('plugin-install.php'));
     491        $btn_txt = 'Install Analytics';
     492    } else {
     493        $btn_url = 'https://wordpress.org/plugins/devdome-analytics/';
     494        $btn_txt = 'View On WordPress.org';
     495    }
     496    $dismiss = wp_nonce_url(add_query_arg('devdcorev1_an_card_dismiss', 1), 'devdcorev1_an_card');
     497    $tools   = add_query_arg(array('page' => DEVDCOREV1_TOOLS_MENU_SLUG), admin_url('admin.php'));
     498    ?>
     499    <style>
     500    .ddan-card{display:flex;align-items:center;gap:16px;margin:16px 24px 0;padding:14px 18px;background:#0b1224;color:#fff;border-radius:12px;position:relative;max-width:1000px;box-sizing:border-box}
     501    .ddan-ic{flex:0 0 42px;width:42px;height:42px;border-radius:10px;background:#4f46e5;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:16px;letter-spacing:-.5px}
     502    .ddan-body{flex:1 1 auto;min-width:0}
     503    .ddan-body strong{display:block;font-size:15px;line-height:1.3;margin:0 0 3px;color:#fff;font-weight:800}
     504    .ddan-body span{display:block;font-size:13px;line-height:1.45;color:#fff;font-weight:600}
     505    .ddan-body .ddan-key{margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.18)}
     506    .ddan-body a{color:#c7d2fe;text-decoration:underline;font-weight:700}
     507    .ddan-body a:hover{text-decoration:underline}
     508    .ddan-btn{flex:0 0 auto;background:#4f46e5;color:#fff;font-weight:700;font-size:13px;padding:9px 16px;border-radius:9px;text-decoration:none;white-space:nowrap}
     509    .ddan-btn:hover,.ddan-btn:focus{background:#4338ca;color:#fff}
     510    .ddan-x{position:absolute;top:6px;right:10px;color:#c7d2fe;text-decoration:none;font-size:18px;line-height:1}
     511    .ddan-x:hover{color:#fff}
     512    @media (max-width:782px){.ddan-card{flex-wrap:wrap;margin:12px 10px 0}.ddan-btn{width:100%;text-align:center}}
     513    </style>
     514    <div class="ddan-card">
     515        <span class="ddan-ic">DD</span>
     516        <div class="ddan-body">
     517            <strong>See your real visitors, not the bots. Free up to 50,000 pageviews a month.</strong>
     518            <span>DevDome Analytics shows how many real people visit your site and how many hits are bots, with no cookies.</span>
     519            <span>More DevDome plugins block bad bots, remove malware, clean unused media, find broken links, manage redirects and affiliate links, tidy the admin and log what users do. <a href="<?php echo esc_url($tools); ?>">See all plugins</a></span>
     520            <span class="ddan-key">One DevDome account is the key to everything: connect it once and every plugin gets its dashboard and email alerts.</span>
     521        </div>
     522        <a class="ddan-btn" href="<?php echo esc_url($btn_url); ?>"><?php echo esc_html($btn_txt); ?></a>
     523        <a class="ddan-x" href="<?php echo esc_url($dismiss); ?>" title="Dismiss" aria-label="Dismiss">&times;</a>
     524    </div>
     525    <?php
     526}
     527
    456528function devdcorev1_hub_render_default()
    457529{
  • devdome-admin-cleaner/trunk/lib/devdome-core/loader.php

    r3721976 r3750420  
    5353}
    5454
    55 DEVDCOREV1_Loader::register('1.7.10', __DIR__ . '/devdome-core.php');
     55DEVDCOREV1_Loader::register('1.7.13', __DIR__ . '/devdome-core.php');
  • devdome-admin-cleaner/trunk/lib/devdome-core/uninstall.php

    r3704110 r3750420  
    4242        delete_option('devdcorev1_inventory_consent');
    4343        delete_option('devdcorev1_connection_gen');
     44        // Core audit 2026-10-16: the Analytics mention card dismissal (1.7.13), the update-heal records
     45        // (1.7.2) and the per-user "Report a bug" bubble flag (1.7.0) were never swept.
     46        delete_option('devdcorev1_an_card_dismissed');
     47        delete_option('devdcorev1_heal_leftovers');
     48        delete_option('devdcorev1_heal_untracked');
     49        delete_metadata('user', 0, 'devdcorev1_bug_hint_seen', '', true);
    4450        // The connection itself (core 1.6.1): identifiers, the site token, the cached account
    4551        // and connection state, and every short-lived connect/beacon transient.
    46         foreach (array('devdcorev1_site_id', 'devdcorev1_site_token', 'devdcorev1_account_id', 'devdcorev1_account_email', 'devdcorev1_account', 'devdcorev1_conn_state', 'devdcorev1_conn_checked', 'devdcorev1_hub_catalog') as $opt) {
     52        foreach (array('devdcorev1_telemetry', 'devdcorev1_telemetry_salt', 'devdcorev1_site_id', 'devdcorev1_site_token', 'devdcorev1_account_id', 'devdcorev1_account_email', 'devdcorev1_account', 'devdcorev1_conn_state', 'devdcorev1_conn_checked', 'devdcorev1_hub_catalog') as $opt) {
    4753            delete_option($opt);
    4854        }
  • devdome-admin-cleaner/trunk/readme.txt

    r3721976 r3750420  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.9
     7Stable tag: 1.0.10
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    190190
    191191== Changelog ==
     192
     193= 1.0.10 =
     194
     195* A dismissible note at the top of the settings page mentions the free DevDome Analytics plugin, with a button that opens the plugin installer (or the Analytics screen when it is already active). Dismiss hides it for good.
     196* Shared DevDome library 1.7.13: uninstalling the last DevDome plugin also removes the note's dismissal, the update-repair records and the per-user "Report a bug" flag; the visitor-beacon flood guard keeps one short-lived record per address and clears its expired ones itself, and behind Cloudflare it counts per visitor instead of per Cloudflare address.
     197* Shared DevDome library 1.7.12: a Cloudflare visitor address header is trusted only when the connection itself comes from a Cloudflare address, so a forged header from anywhere else is ignored.
     198* Shared DevDome library 1.7.11: on a subdirectory multisite, a site mapped to its own domain is its own site and no longer shares the network's connection.
     199* Deleting the plugin also removes the cached update-check answer it kept in the database.
    192200
    193201= 1.0.9 =
    … …  
    272280
    273281* Initial release: one-click admin cleanup with a live clutter score, a notice inbox with snooze/mute/restore (criticals always kept visible), per-widget dashboard cleanup, admin-bar and footer/version hiding, role-based Client Mode, and five ready-made presets.
     282
     283== Upgrade Notice ==
     284
     285= 1.0.10 =
     286Housekeeping release. Nothing changes on the screen.
     287
     288= 1.0.9 =
     289Shared DevDome library 1.7.10 and WordPress.org review fixes (escaping, no output buffering). Settings are unaffected.
  • devdome-admin-cleaner/trunk/uninstall.php

    r3704110 r3750420  
    4444// plugin still installed, so removing this one never orphans the core for the others).
    4545require_once __DIR__ . '/lib/devdome-core/uninstall.php';
     46// The suite updater caches its update manifest under a hashed key, per site and (on multisite) network wide.
     47// Clear both spellings, or the cache outlives the plugin.
     48$devdadcl_updater_cache = 'devdome_upd_' . md5('devdome-admin-cleaner');
     49delete_transient($devdadcl_updater_cache);
     50if (is_multisite()) {
     51    delete_site_transient($devdadcl_updater_cache);
     52}
    4653devdcorev1_uninstall_cleanup('devdome-admin-cleaner/devdome-admin-cleaner.php');
    4754
Note: See TracChangeset for help on using the changeset viewer.