false]) : $value; } } if (!function_exists('forge_kses_credit_badge')) { /** * Allowed-HTML list for echoing trusted, self-built credit-cost badge and * AI-button inner markup at output time (covers the inline SVG icon that * wp_kses_post would strip). Every dynamic value in that markup is already * esc_attr'd / integer-cast by its builder; this is the late-escape gate. */ function forge_kses_credit_badge() { $attr = array( 'class' => true, 'style' => true, 'title' => true, 'id' => true, 'role' => true, 'aria-hidden' => true, 'aria-label' => true, 'data-cost' => true, 'data-action' => true, 'data-slug' => true, ); return array( 'span' => $attr, 'strong' => $attr, 'small' => $attr, 'b' => $attr, 'em' => $attr, 'svg' => array('class'=>true,'style'=>true,'width'=>true,'height'=>true,'viewbox'=>true,'fill'=>true,'stroke'=>true,'xmlns'=>true,'aria-hidden'=>true,'role'=>true,'focusable'=>true), 'path' => array('d'=>true,'fill'=>true,'fill-rule'=>true,'clip-rule'=>true,'stroke'=>true,'stroke-width'=>true,'stroke-linecap'=>true,'stroke-linejoin'=>true,'opacity'=>true), 'g' => array('fill'=>true,'stroke'=>true,'transform'=>true), 'circle' => array('cx'=>true,'cy'=>true,'r'=>true,'fill'=>true,'stroke'=>true), 'rect' => array('x'=>true,'y'=>true,'width'=>true,'height'=>true,'rx'=>true,'fill'=>true), ); } } if ( ! defined( 'FORGE_DOCS_GUARDFORGE' ) ) { define( 'FORGE_DOCS_GUARDFORGE', 'https://guardforge.app/docs/' ); } // Pro Bridge — the Free side of the Free/Pro extension contract. Loaded before // anything downstream so it can consult it. The Pro add-on (guardforge-pro) // registers + enables features against this bridge. require_once GUARDFORGE_DIR . 'includes/class-pro-bridge.php'; GUARDFORGE_Pro_Bridge::instance(); spl_autoload_register( function ( $class ) { $prefix = 'GUARDFORGE_'; if ( strpos( $class, $prefix ) !== 0 ) { return; } $name = strtolower( str_replace( '_', '-', substr( $class, strlen( $prefix ) ) ) ); $candidates = [ GUARDFORGE_DIR . 'includes/class-' . $name . '.php', GUARDFORGE_DIR . 'admin/class-' . $name . '.php', ]; foreach ( $candidates as $file ) { if ( file_exists( $file ) ) { require_once $file; return; } } } ); // NOTE: Everything about licences, checkout and the update channel lives // exclusively in the Pro add-on (guardforge-pro); since 1.6.0 that includes the // licence class itself. This package asks only whether the add-on is installed // and what it switched on (GUARDFORGE_Pro_Bridge) and ships no licence-key // handling of any kind. // Forge Queue — load before plugins_loaded so the table exists early. if ( ! class_exists( 'Forge_Queue' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-queue.php'; } // Shared transactional email renderer (first Forge plugin to load wins). if ( ! class_exists( 'Forge_Email_Template' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-email.php'; } // Shared SSRF-safe wp_remote_* wrapper (first Forge plugin to load wins). if ( ! class_exists( 'Forge_Net' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-net.php'; } // Shared opt-in telemetry ships in Pro; load it here only when Pro put it on // disk, so a Free-only site never carries an update/telemetry channel. if ( ! class_exists( 'Forge_Telemetry' ) && file_exists( GUARDFORGE_DIR . 'includes/class-forge-telemetry.php' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-telemetry.php'; } add_action( 'plugins_loaded', function () { require_once GUARDFORGE_DIR . 'includes/class-schema-manager.php'; GUARDFORGE_Schema_Manager::maybe_upgrade(); }, 1 ); function guardforge_init() { /* * No load_plugin_textdomain for 'guardforge'. Since WP 4.6 the first * __ against a registered domain loads the catalogue just in time, from * wp-content/languages/plugins/ first and then from the path in this * file's `Domain Path: /languages` header — which covers the bundled .mo * on the Avakode channel as well as the community packs on wp.org. The * call was also on plugins_loaded rather than init, which is the other * half of the wp.org note about it. * * The alias below is a different function for a different domain and has * to stay: 'forge' belongs to no plugin, so nothing loads it for us. */ // Shared Forge UI (class-forge-*.php) uses the 'forge' textdomain — alias it // to this plugin's own compiled catalog so the shared dashboard/credits/ // connect/feedback strings translate. Prefer the community language pack // from translate.wordpress.org when one exists. $gf_forge_locale = determine_locale(); $gf_forge_mo = WP_LANG_DIR . '/plugins/guardforge-' . $gf_forge_locale . '.mo'; if ( ! is_readable( $gf_forge_mo ) ) { $gf_forge_mo = GUARDFORGE_DIR . 'languages/guardforge-' . $gf_forge_locale . '.mo'; } if ( is_readable( $gf_forge_mo ) ) { load_textdomain( 'forge', $gf_forge_mo ); } // An add-on too old for this core is kept from booting (GUARDFORGE_MIN_PRO). GUARDFORGE_Pro_Bridge::keep_outdated_addon_from_booting(); GUARDFORGE_Core::instance(); // Before every module that can act on the outside world, because it is // the one they ask whether they should. GUARDFORGE_Staging::instance(); GUARDFORGE_Hardening::instance(); GUARDFORGE_Files_Protector::instance(); GUARDFORGE_Spam_Bots::instance(); GUARDFORGE_Bruteforce::instance(); GUARDFORGE_Integrity::instance(); GUARDFORGE_Audit_Log::instance(); if ( class_exists( 'GUARDFORGE_Event_Explainer' ) ) { GUARDFORGE_Event_Explainer::instance(); } GUARDFORGE_Csp::instance(); // The public advisory feed, seven days behind the licensed one. It hooks // the same daily cron the add-on does and stands down when the add-on is // installed and licensed — see class-vuln-feed.php. // class_exists rather than an unconditional call: this feed's entire // function is a request to an Avakode server, and the wp.org build makes // no such request, so GUARDFORGE_Vuln_Feed is not in that package. Booting // it there would fatal on every page load of the site. if ( class_exists( 'GUARDFORGE_Vuln_Feed' ) ) { GUARDFORGE_Vuln_Feed::instance(); } // The per-file core restore, for the same reason and in the same shape: // it writes under ABSPATH, which the plugin directory does not allow, so // the wp.org package does not carry class-core-restore.php. Without it // the integrity screen reports the same findings and offers no button. if ( class_exists( 'GUARDFORGE_Core_Restore' ) ) { GUARDFORGE_Core_Restore::instance(); } // §6.4 K1-a/b/c — cases built from the observations the modules above // already write (integrity scans, the vulnerability mirror, the login // log). After them in this list for exactly that reason: sync reads // their options/tables and creates nothing new to observe. GUARDFORGE_Cases::instance(); GUARDFORGE_Case_Actions::instance(); // §6.4 K1-f — the monthly report over those cases. Booted here only for // its admin-post download handler; the numbers themselves are read on the // Cases screen's own render. GUARDFORGE_Guard_Report_Export::instance(); // §7 FR part 5 — the same cases, handed to the Suite dashboard's first // screen. A filter callback only: it reads when that screen renders, and // never on a request of its own. GUARDFORGE_Results_Provider::instance(); GUARDFORGE_Hardening_Score::instance(); GUARDFORGE_Hardening_Score_Badge::instance(); // Two-factor lives here from 1.1.0 on. GuardForge Pro 1.0.x shipped its // own GUARDFORGE_Two_Factor and require_once's it at plugins_loaded:6 — // declaring ours first would fatal the site on the duplicate class. If a // stale add-on is still on disk, stand down and let it run its copy; the // Pro update replaces that file within the same release. $guardforge_legacy_2fa = defined( 'GUARDFORGE_PRO_DIR' ) && file_exists( GUARDFORGE_PRO_DIR . 'includes/class-two-factor.php' ); if ( ! $guardforge_legacy_2fa ) { GUARDFORGE_Two_Factor::instance(); } // Pro modules (Alerts, Lockdown, VulnDB, Geo-IP, Cloudflare Sync, WAF, // Malware Scanner, AI Threat, and the 2FA enforcement policy) live in // the guardforge-pro add-on. // Its loader (GUARDFORGE_Pro_Loader::boot) require_once's and instantiates // them at plugins_loaded priority 6. On a Free-only site they are simply // absent. GUARDFORGE_Admin::instance(); // Forge Queue handlers — Free side. Forge_Queue::instance(); if ( is_admin() ) { $guardforge_dashboard_file = GUARDFORGE_DIR . 'includes/class-forge-dashboard.php'; if ( is_readable( $guardforge_dashboard_file ) ) { require_once $guardforge_dashboard_file; if ( class_exists( 'Forge_Suite_Dashboard' ) ) { Forge_Suite_Dashboard::instance(); // Register AJAX hooks early (admin-ajax.php). } } require_once GUARDFORGE_DIR . 'includes/class-forge-docs-link.php'; require_once GUARDFORGE_DIR . 'includes/class-forge-support-link.php'; // The shared Pro tip is Avakode-channel only (the wp.org package leaves // it out), so it loads only where it ships. $guardforge_pro_tip_file = GUARDFORGE_DIR . 'includes/class-forge-pro-tip.php'; if ( is_readable( $guardforge_pro_tip_file ) ) { require_once $guardforge_pro_tip_file; } $guardforge_feedback_file = GUARDFORGE_DIR . 'includes/class-forge-feedback-widget.php'; if ( is_readable( $guardforge_feedback_file ) ) { require_once $guardforge_feedback_file; } // The connection of a site to an Avakode account's credits (the AJAX handlers and the return // from avakode.com). It belongs to the AI client: the wp.org package has neither, so it has // no connection either. if ( class_exists( 'GUARDFORGE_Forge_API' ) ) { $guardforge_credits = GUARDFORGE_Forge_API::credits_connect(); if ( $guardforge_credits ) { $guardforge_credits->register(); } } } } add_action( 'plugins_loaded', 'guardforge_init', 5 ); register_activation_hook( __FILE__, function () { require_once GUARDFORGE_DIR . 'includes/class-schema-manager.php'; GUARDFORGE_Schema_Manager::maybe_upgrade(); // guardforge_integrity_cron is not scheduled here by a wp_schedule_event // of its own (review of 23.09.2026). The scan asks api.wordpress.org for the // official checksums, so it is a request activation may not make and may // not put on a schedule on its own account: it goes on when the owner // switches the file-integrity monitor on — see // GUARDFORGE_Integrity::apply_consent. // What activation does is give back what that switch already asks for. // Deactivation (below) takes the cron off, so an owner whose switch says // "on", who deactivates and activates again, would otherwise have a monitor // that is on and a scan that never comes in until Settings is next saved. // apply_consent is the call the save makes: with the switch off — every // fresh install — it schedules nothing. // A note for the reviewer: when the monitor is already on, the scan comes in // about a minute after activation. That is the owner's earlier consent being // honoured, given in Settings before the plugin was deactivated; it is not // something activation starts on its own account. Activation itself makes no // request, and a switch that is off, which is every fresh install, leaves // nothing on the schedule. GUARDFORGE_Integrity::apply_consent(); if ( ! wp_next_scheduled( 'guardforge_log_gc_cron' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'guardforge_log_gc_cron' ); } // guardforge_vulndb_cron is deliberately absent from this list. It is the // one scheduled task that reaches an Avakode server, and Guideline 7 says // activation may neither make such a request nor schedule one on its own // account. It goes on the schedule when the owner ticks "Download the // public vulnerability feed" in Settings — see // GUARDFORGE_Vuln_Feed::apply_consent. // What activation gives back is the event that deactivation (below) took // off, and only for an owner whose switch already says on: the same // restoration as the monitor's scan above, under the same rule. With the // switch off schedule_if_consented does nothing at all, and it never // touches what was downloaded (apply_consent's "off" branch does, which // is why that one is not called here). The class is not in the wp.org // package, which makes no such request; hence the class_exists. if ( class_exists( 'GUARDFORGE_Vuln_Feed' ) ) { GUARDFORGE_Vuln_Feed::schedule_if_consented(); } if ( ! wp_next_scheduled( 'guardforge_score_cron' ) ) { wp_schedule_event( time() + 3 * HOUR_IN_SECONDS, 'daily', 'guardforge_score_cron' ); } // §6.4: builds/updates cases from the scans and logs above. After all // four in the schedule, so the first run of the day has something to // read. if ( ! wp_next_scheduled( 'guardforge_cases_cron' ) ) { wp_schedule_event( time() + 4 * HOUR_IN_SECONDS, 'daily', 'guardforge_cases_cron' ); } if ( file_exists( GUARDFORGE_DIR . 'includes/class-files-protector.php' ) ) { require_once GUARDFORGE_DIR . 'includes/class-files-protector.php'; GUARDFORGE_Files_Protector::write_htaccess_rules(); } if ( file_exists( GUARDFORGE_DIR . 'includes/class-forge-telemetry.php' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-telemetry.php'; if ( class_exists( 'Forge_Telemetry' ) ) { $prev = (string) get_option( 'guardforge_previous_version', '' ); Forge_Telemetry::track( 'plugin.activated', [ 'first_install' => $prev === '', 'previous_version' => $prev, ] ); update_option( 'guardforge_previous_version', GUARDFORGE_VERSION, false ); if ( ! get_option( 'guardforge_installed_at' ) ) { update_option( 'guardforge_installed_at', time(), false ); } } } } ); register_deactivation_hook( __FILE__, function () { wp_clear_scheduled_hook( 'guardforge_integrity_cron' ); wp_clear_scheduled_hook( 'guardforge_log_gc_cron' ); wp_clear_scheduled_hook( 'guardforge_vulndb_cron' ); wp_clear_scheduled_hook( 'guardforge_score_cron' ); wp_clear_scheduled_hook( 'guardforge_cases_cron' ); if ( file_exists( GUARDFORGE_DIR . 'includes/class-files-protector.php' ) ) { require_once GUARDFORGE_DIR . 'includes/class-files-protector.php'; GUARDFORGE_Files_Protector::remove_htaccess_rules(); } if ( file_exists( GUARDFORGE_DIR . 'includes/class-forge-telemetry.php' ) ) { require_once GUARDFORGE_DIR . 'includes/class-forge-telemetry.php'; if ( class_exists( 'Forge_Telemetry' ) ) { $installed_at = (int) get_option( 'guardforge_installed_at', time() ); Forge_Telemetry::track( 'plugin.deactivated', [ 'uptime_days' => max( 0, (int) floor( ( time() - $installed_at ) / DAY_IN_SECONDS ) ), ] ); Forge_Telemetry::flush(); } } } );