modified file smtp-mailer
This commit is contained in:
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle Rewind
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Class that handles the rewind api call.
|
||||
*/
|
||||
class Credentials {
|
||||
/**
|
||||
* Get the rewind state, if no creds are set the state will be 'awaiting_for_credentials'
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function get_credential_array() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
$is_connected = ( new Connection_Manager() )->is_connected();
|
||||
|
||||
if ( ! $blog_id || ! $is_connected ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api_url = sprintf( '/sites/%d/scan', $blog_id );
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
$api_url,
|
||||
'2',
|
||||
array( 'method' => 'GET' ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parsed_response = json_decode( $response['body'] );
|
||||
|
||||
if ( ! $parsed_response ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset( $parsed_response->credentials ) ? $parsed_response->credentials : array();
|
||||
}
|
||||
}
|
@ -0,0 +1,456 @@
|
||||
<?php
|
||||
/**
|
||||
* Primary class file for the Jetpack Protect plugin.
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Automattic\Jetpack\Admin_UI\Admin_Menu;
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
use Automattic\Jetpack\JITMS\JITM;
|
||||
use Automattic\Jetpack\Modules;
|
||||
use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
|
||||
use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
|
||||
use Automattic\Jetpack\Plugins_Installer;
|
||||
use Automattic\Jetpack\Protect\Plan;
|
||||
use Automattic\Jetpack\Protect\REST_Controller;
|
||||
use Automattic\Jetpack\Protect\Site_Health;
|
||||
use Automattic\Jetpack\Protect\Status;
|
||||
use Automattic\Jetpack\Status as Jetpack_Status;
|
||||
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
|
||||
use Automattic\Jetpack\Sync\Sender;
|
||||
use Automattic\Jetpack\Waf\Waf_Runner;
|
||||
use Automattic\Jetpack\Waf\Waf_Stats;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Protect
|
||||
*/
|
||||
class Jetpack_Protect {
|
||||
|
||||
/**
|
||||
* Licenses product ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const JETPACK_SCAN_PRODUCT_IDS = array(
|
||||
2010, // JETPACK_SECURITY_DAILY.
|
||||
2011, // JETPACK_SECURITY_DAILY_MOTNHLY.
|
||||
2012, // JETPACK_SECURITY_REALTIME.
|
||||
2013, // JETPACK_SECURITY_REALTIME_MONTHLY.
|
||||
2014, // JETPACK_COMPLETE.
|
||||
2015, // JETPACK_COMPLETE_MONTHLY.
|
||||
2016, // JETPACK_SECURITY_TIER_1_YEARLY.
|
||||
2017, // JETPACK_SECURITY_TIER_1_MONTHLY.
|
||||
2019, // JETPACK_SECURITY_TIER_2_YEARLY.
|
||||
2020, // JETPACK_SECURITY_TIER_2_MONTHLY.
|
||||
2106, // JETPACK_SCAN.
|
||||
2107, // JETPACK_SCAN_MONTHLY.
|
||||
2108, // JETPACK_SCAN_REALTIME.
|
||||
2109, // JETPACK_SCAN_REALTIME_MONTHLY.
|
||||
);
|
||||
const JETPACK_WAF_MODULE_SLUG = 'waf';
|
||||
const JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG = 'protect';
|
||||
const JETPACK_PROTECT_ACTIVATION_OPTION = JETPACK_PROTECT_SLUG . '_activated';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'init' ) );
|
||||
add_action( '_admin_menu', array( $this, 'admin_page_init' ) );
|
||||
|
||||
// Activate the module as the plugin is activated
|
||||
add_action( 'admin_init', array( $this, 'do_plugin_activation_activities' ) );
|
||||
|
||||
// Init Jetpack packages
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
function () {
|
||||
$config = new Automattic\Jetpack\Config();
|
||||
// Connection package.
|
||||
$config->ensure(
|
||||
'connection',
|
||||
array(
|
||||
'slug' => JETPACK_PROTECT_SLUG,
|
||||
'name' => JETPACK_PROTECT_NAME,
|
||||
'url_info' => JETPACK_PROTECT_URI,
|
||||
)
|
||||
);
|
||||
// Sync package.
|
||||
$config->ensure(
|
||||
'sync',
|
||||
array(
|
||||
'jetpack_sync_modules' => array(
|
||||
'Automattic\\Jetpack\\Sync\\Modules\\Options',
|
||||
'Automattic\\Jetpack\\Sync\\Modules\\Callables',
|
||||
'Automattic\\Jetpack\\Sync\\Modules\\Users',
|
||||
),
|
||||
'jetpack_sync_callable_whitelist' => array(
|
||||
'main_network_site' => array( 'Automattic\\Jetpack\\Connection\\Urls', 'main_network_site_url' ),
|
||||
'get_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
|
||||
'get_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
|
||||
'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
|
||||
),
|
||||
'jetpack_sync_options_contentless' => array(),
|
||||
'jetpack_sync_options_whitelist' => array(
|
||||
'active_plugins',
|
||||
'stylesheet',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Identity crisis package.
|
||||
$config->ensure( 'identity_crisis' );
|
||||
|
||||
// Web application firewall package.
|
||||
$config->ensure( 'waf' );
|
||||
},
|
||||
1
|
||||
);
|
||||
|
||||
add_filter( 'jetpack_connection_user_has_license', array( $this, 'jetpack_check_user_licenses' ), 10, 3 );
|
||||
|
||||
add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'protect_filter_available_modules' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 65 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
|
||||
|
||||
REST_Controller::init();
|
||||
My_Jetpack_Initializer::init();
|
||||
Site_Health::init();
|
||||
|
||||
// Sets up JITMS.
|
||||
JITM::configure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the admin page resources.
|
||||
*/
|
||||
public function admin_page_init() {
|
||||
$total_threats = Status::get_total_threats();
|
||||
$menu_label = _x( 'Protect', 'The Jetpack Protect product name, without the Jetpack prefix', 'jetpack-protect' );
|
||||
if ( $total_threats ) {
|
||||
$menu_label .= sprintf( ' <span class="update-plugins">%d</span>', $total_threats );
|
||||
}
|
||||
|
||||
$page_suffix = Admin_Menu::add_menu(
|
||||
__( 'Jetpack Protect', 'jetpack-protect' ),
|
||||
$menu_label,
|
||||
'manage_options',
|
||||
'jetpack-protect',
|
||||
array( $this, 'plugin_settings_page' )
|
||||
);
|
||||
|
||||
add_action( 'load-' . $page_suffix, array( $this, 'enqueue_admin_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the wp-admin styles (used outside the React app)
|
||||
*/
|
||||
public function enqueue_admin_styles() {
|
||||
wp_enqueue_style( 'jetpack-protect-wpadmin', JETPACK_PROTECT_BASE_PLUGIN_URL . '/assets/jetpack-protect.css', array(), JETPACK_PROTECT_VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue plugin admin scripts and styles.
|
||||
*/
|
||||
public function enqueue_admin_scripts() {
|
||||
|
||||
Assets::register_script(
|
||||
'jetpack-protect',
|
||||
'build/index.js',
|
||||
JETPACK_PROTECT_ROOT_FILE,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-protect',
|
||||
)
|
||||
);
|
||||
Assets::enqueue_script( 'jetpack-protect' );
|
||||
// Required for Analytics.
|
||||
wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
|
||||
// Initial JS state including JP Connection data.
|
||||
Connection_Initial_State::render_script( 'jetpack-protect' );
|
||||
wp_add_inline_script( 'jetpack-protect', $this->render_initial_state(), 'before' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state into a JavaScript variable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render_initial_state() {
|
||||
return 'var jetpackProtectInitialState=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( $this->initial_state() ) ) . '"));';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initial state data for hydrating the React UI.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function initial_state() {
|
||||
global $wp_version;
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$refresh_status_from_wpcom = isset( $_GET['checkPlan'] );
|
||||
$initial_state = array(
|
||||
'apiRoot' => esc_url_raw( rest_url() ),
|
||||
'apiNonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
|
||||
'status' => Status::get_status( $refresh_status_from_wpcom ),
|
||||
'installedPlugins' => Plugins_Installer::get_plugins(),
|
||||
'installedThemes' => Sync_Functions::get_themes(),
|
||||
'wpVersion' => $wp_version,
|
||||
'adminUrl' => 'admin.php?page=jetpack-protect',
|
||||
'siteSuffix' => ( new Jetpack_Status() )->get_site_suffix(),
|
||||
'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ),
|
||||
'hasRequiredPlan' => Plan::has_required_plan(),
|
||||
'waf' => array(
|
||||
'wafSupported' => Waf_Runner::is_supported_environment(),
|
||||
'currentIp' => IP_Utils::get_ip(),
|
||||
'isSeen' => self::get_waf_seen_status(),
|
||||
'upgradeIsSeen' => self::get_waf_upgrade_seen_status(),
|
||||
'displayUpgradeBadge' => self::get_waf_upgrade_badge_display_status(),
|
||||
'isEnabled' => Waf_Runner::is_enabled(),
|
||||
'isToggling' => false,
|
||||
'isUpdating' => false,
|
||||
'config' => Waf_Runner::get_config(),
|
||||
'stats' => self::get_waf_stats(),
|
||||
),
|
||||
);
|
||||
|
||||
$initial_state['jetpackScan']['pricingForUi'] = Plan::get_product( 'jetpack_scan' );
|
||||
|
||||
return $initial_state;
|
||||
}
|
||||
/**
|
||||
* Main plugin settings page.
|
||||
*/
|
||||
public function plugin_settings_page() {
|
||||
?>
|
||||
<div id="jetpack-protect-root"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the WAF module on plugin activation.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function plugin_activation() {
|
||||
add_option( self::JETPACK_PROTECT_ACTIVATION_OPTION, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on admin_init, and does actions required on plugin activation, based on
|
||||
* the activation option.
|
||||
*
|
||||
* This needs to be run after the activation hook, as that results in a redirect,
|
||||
* and we need the sync module's actions and filters to be registered.
|
||||
*/
|
||||
public static function do_plugin_activation_activities() {
|
||||
if ( get_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ) && ( new Connection_Manager() )->is_connected() ) {
|
||||
self::activate_modules();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the waf and brute force protection modules and disables the activation option
|
||||
*/
|
||||
public static function activate_modules() {
|
||||
delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION );
|
||||
( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false );
|
||||
( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes plugin from the connection manager
|
||||
* If it's the last plugin using the connection, the site will be disconnected.
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
*/
|
||||
public static function plugin_deactivation() {
|
||||
|
||||
// Clear Sync data.
|
||||
Sender::get_instance()->uninstall();
|
||||
|
||||
$manager = new Connection_Manager( 'jetpack-protect' );
|
||||
$manager->remove_connection();
|
||||
|
||||
Status::delete_option();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shortcut on Admin Bar to show the total of threats found.
|
||||
*
|
||||
* @param object $wp_admin_bar The Admin Bar object.
|
||||
* @return void
|
||||
*/
|
||||
public function admin_bar( $wp_admin_bar ) {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$total = Status::get_total_threats();
|
||||
|
||||
if ( $total > 0 ) {
|
||||
$args = array(
|
||||
'id' => 'jetpack-protect',
|
||||
'title' => '<span class="ab-icon jp-protect-icon"></span><span class="ab-label">' . $total . '</span>',
|
||||
'href' => admin_url( 'admin.php?page=jetpack-protect' ),
|
||||
'meta' => array(
|
||||
// translators: %d is the number of threats found.
|
||||
'title' => sprintf( _n( '%d threat found by Jetpack Protect', '%d threats found by Jetpack Protect', $total, 'jetpack-protect' ), $total ),
|
||||
),
|
||||
);
|
||||
|
||||
$wp_admin_bar->add_node( $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds modules to the list of available modules
|
||||
*
|
||||
* @param array $modules The available modules.
|
||||
* @return array
|
||||
*/
|
||||
public function protect_filter_available_modules( $modules ) {
|
||||
return array_merge( array( self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG ), $modules );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has an available license that includes Jetpack Scan.
|
||||
*
|
||||
* @param boolean $has_license Whether a license was already found.
|
||||
* @param object[] $licenses Unattached licenses belonging to the user.
|
||||
* @param string $plugin_slug Slug of the plugin that initiated the flow.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function jetpack_check_user_licenses( $has_license, $licenses, $plugin_slug ) {
|
||||
if ( $plugin_slug !== JETPACK_PROTECT_SLUG || $has_license ) {
|
||||
return $has_license;
|
||||
}
|
||||
|
||||
$license_found = false;
|
||||
|
||||
foreach ( $licenses as $license ) {
|
||||
if ( $license->attached_at || $license->revoked_at ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( in_array( $license->product_id, self::JETPACK_SCAN_PRODUCT_IDS, true ) ) {
|
||||
$license_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $license_found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF "Seen" Status
|
||||
*
|
||||
* @return bool Whether the current user has viewed the WAF screen.
|
||||
*/
|
||||
public static function get_waf_seen_status() {
|
||||
return (bool) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_seen', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WAF "Seen" Status
|
||||
*
|
||||
* @return bool True if seen status updated to true, false on failure.
|
||||
*/
|
||||
public static function set_waf_seen_status() {
|
||||
return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_seen', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF Upgrade "Seen" Status
|
||||
*
|
||||
* @return bool Whether the current user has dismissed the upgrade popover or enabled the automatic rules feature.
|
||||
*/
|
||||
public static function get_waf_upgrade_seen_status() {
|
||||
return (bool) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WAF Upgrade "Seen" Status
|
||||
*
|
||||
* @return bool True if upgrade seen status updated to true, false on failure.
|
||||
*/
|
||||
public static function set_waf_upgrade_seen_status() {
|
||||
self::set_waf_upgrade_badge_timestamp();
|
||||
return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF Upgrade Badge Timestamp
|
||||
*
|
||||
* @return integer The timestamp for the when the upgrade seen status was first set to true.
|
||||
*/
|
||||
public static function get_waf_upgrade_badge_timestamp() {
|
||||
return (int) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WAF Upgrade Badge Timestamp
|
||||
*
|
||||
* @return bool True if upgrade badge timestamp to set to the current time, false on failure.
|
||||
*/
|
||||
public static function set_waf_upgrade_badge_timestamp() {
|
||||
return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF Upgrade Badge Display Status
|
||||
*
|
||||
* @return bool True if upgrade badge timestamp is set and less than 7 days ago, otherwise false.
|
||||
*/
|
||||
public static function get_waf_upgrade_badge_display_status() {
|
||||
$badge_timestamp_exists = metadata_exists( 'user', get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp' );
|
||||
if ( ! $badge_timestamp_exists ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$badge_timestamp = self::get_waf_upgrade_badge_timestamp();
|
||||
$seven_days = strtotime( '-7 days' );
|
||||
if ( $badge_timestamp > $seven_days ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF stats
|
||||
*
|
||||
* @return bool|array False if WAF is not enabled, otherwise an array of stats.
|
||||
*/
|
||||
public static function get_waf_stats() {
|
||||
if ( ! Waf_Runner::is_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'ipAllowListCount' => Waf_Stats::get_ip_allow_list_count(),
|
||||
'ipBlockListCount' => Waf_Stats::get_ip_block_list_count(),
|
||||
'rulesVersion' => Waf_Stats::get_rules_version(),
|
||||
'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle the Protect plan
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Current_Plan;
|
||||
|
||||
/**
|
||||
* The Plan class.
|
||||
*/
|
||||
class Plan {
|
||||
/**
|
||||
* The meta name used to store the cache date
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_DATE_META_NAME = 'protect-cache-date';
|
||||
|
||||
/**
|
||||
* Valid pediord for the cache: One week.
|
||||
*/
|
||||
const CACHE_VALIDITY_PERIOD = 7 * DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* The meta name used to store the cache
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_META_NAME = 'protect-cache';
|
||||
|
||||
/**
|
||||
* Checks if the cache is old, meaning we need to fetch new data from WPCOM
|
||||
*/
|
||||
private static function is_cache_old() {
|
||||
if ( empty( self::get_product_from_cache() ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cache_date = get_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, true );
|
||||
return time() - (int) $cache_date > ( self::CACHE_VALIDITY_PERIOD );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product list from the user cache
|
||||
*/
|
||||
private static function get_product_from_cache() {
|
||||
return get_user_meta( get_current_user_id(), self::CACHE_META_NAME, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product data
|
||||
*
|
||||
* @param string $wpcom_product The product slug.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product( $wpcom_product = 'jetpack_scan' ) {
|
||||
if ( ! self::is_cache_old() ) {
|
||||
return self::get_product_from_cache();
|
||||
}
|
||||
|
||||
$request_url = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
|
||||
$wpcom_request = wp_remote_get( esc_url_raw( $request_url ) );
|
||||
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
|
||||
|
||||
if ( 200 === $response_code ) {
|
||||
$products = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
|
||||
|
||||
// Pick the desired product...
|
||||
$product = $products->{$wpcom_product};
|
||||
|
||||
// ... and store it into the cache.
|
||||
update_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, time() );
|
||||
update_user_meta( get_current_user_id(), self::CACHE_META_NAME, $product );
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'failed_to_fetch_data',
|
||||
esc_html__( 'Unable to fetch the requested data.', 'jetpack-protect' ),
|
||||
array(
|
||||
'status' => $response_code,
|
||||
'request' => $wpcom_request,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Has Required Plan
|
||||
*
|
||||
* @param bool $force_refresh Refresh the local plan cache from wpcom.
|
||||
* @return bool True when the site has a plan or product that supports the paid Protect tier.
|
||||
*/
|
||||
public static function has_required_plan( $force_refresh = false ) {
|
||||
static $has_scan = null;
|
||||
if ( null === $has_scan || $force_refresh ) {
|
||||
$products = array_column( Current_Plan::get_products(), 'product_slug' );
|
||||
|
||||
// Check for a plan or product that enables scan.
|
||||
$plan_supports_scan = Current_Plan::supports( 'scan', true );
|
||||
$has_scan_product = count( array_intersect( array( 'jetpack_scan', 'jetpack_scan_monthly' ), $products ) ) > 0;
|
||||
$has_scan = $plan_supports_scan || $has_scan_product;
|
||||
}
|
||||
|
||||
return $has_scan;
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle the Protect Status of Jetpack Protect
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Plugins_Installer;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class that handles fetching and caching the Status of vulnerabilities check from the WPCOM servers
|
||||
*/
|
||||
class Protect_Status extends Status {
|
||||
|
||||
/**
|
||||
* WPCOM endpoint
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REST_API_BASE = '/sites/%d/jetpack-protect-status';
|
||||
|
||||
/**
|
||||
* Name of the option where status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'jetpack_protect_status';
|
||||
|
||||
/**
|
||||
* Name of the option where the timestamp of the status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_TIMESTAMP_NAME = 'jetpack_protect_status_time';
|
||||
|
||||
/**
|
||||
* Gets the current status of the Jetpack Protect checks
|
||||
*
|
||||
* @param bool $refresh_from_wpcom Refresh the local plan and status cache from wpcom.
|
||||
* @return Status_Model
|
||||
*/
|
||||
public static function get_status( $refresh_from_wpcom = false ) {
|
||||
if ( self::$status !== null ) {
|
||||
return self::$status;
|
||||
}
|
||||
|
||||
if ( $refresh_from_wpcom || ! self::should_use_cache() || self::is_cache_expired() ) {
|
||||
$status = self::fetch_from_server();
|
||||
} else {
|
||||
$status = self::get_from_options();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $status ) ) {
|
||||
$status = new Status_Model(
|
||||
array(
|
||||
'error' => true,
|
||||
'error_code' => $status->get_error_code(),
|
||||
'error_message' => $status->get_error_message(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$status = self::normalize_protect_report_data( $status );
|
||||
}
|
||||
|
||||
self::$status = $status;
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the WPCOM API endpoint
|
||||
*
|
||||
* @return WP_Error|string
|
||||
*/
|
||||
public static function get_api_url() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
$is_connected = ( new Connection_Manager() )->is_connected();
|
||||
|
||||
if ( ! $blog_id || ! $is_connected ) {
|
||||
return new WP_Error( 'site_not_connected' );
|
||||
}
|
||||
|
||||
$api_url = sprintf( self::REST_API_BASE, $blog_id );
|
||||
|
||||
return $api_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the status from WPCOM servers
|
||||
*
|
||||
* @return WP_Error|array
|
||||
*/
|
||||
public static function fetch_from_server() {
|
||||
$api_url = self::get_api_url();
|
||||
if ( is_wp_error( $api_url ) ) {
|
||||
return $api_url;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
self::get_api_url(),
|
||||
'2',
|
||||
array( 'method' => 'GET' ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) {
|
||||
return new WP_Error( 'failed_fetching_status', 'Failed to fetch Protect Status data from server', array( 'status' => $response_code ) );
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
self::update_status_option( $body );
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize data from the Protect Report data source.
|
||||
*
|
||||
* @param object $report_data Data from the Protect Report.
|
||||
* @return Status_Model
|
||||
*/
|
||||
protected static function normalize_protect_report_data( $report_data ) {
|
||||
$status = new Status_Model();
|
||||
$status->data_source = 'protect_report';
|
||||
|
||||
// map report data properties directly into the Status_Model
|
||||
$status->status = isset( $report_data->status ) ? $report_data->status : null;
|
||||
$status->last_checked = isset( $report_data->last_checked ) ? $report_data->last_checked : null;
|
||||
$status->num_threats = isset( $report_data->num_vulnerabilities ) ? $report_data->num_vulnerabilities : null;
|
||||
$status->num_themes_threats = isset( $report_data->num_themes_vulnerabilities ) ? $report_data->num_themes_vulnerabilities : null;
|
||||
$status->num_plugins_threats = isset( $report_data->num_plugins_vulnerabilities ) ? $report_data->num_plugins_vulnerabilities : null;
|
||||
|
||||
// merge plugins from report with all installed plugins before mapping into the Status_Model
|
||||
$installed_plugins = Plugins_Installer::get_plugins();
|
||||
$last_report_plugins = isset( $report_data->plugins ) ? $report_data->plugins : new \stdClass();
|
||||
$status->plugins = self::merge_installed_and_checked_lists( $installed_plugins, $last_report_plugins, array( 'type' => 'plugins' ) );
|
||||
|
||||
// merge themes from report with all installed plugins before mapping into the Status_Model
|
||||
$installed_themes = Sync_Functions::get_themes();
|
||||
$last_report_themes = isset( $report_data->themes ) ? $report_data->themes : new \stdClass();
|
||||
$status->themes = self::merge_installed_and_checked_lists( $installed_themes, $last_report_themes, array( 'type' => 'themes' ) );
|
||||
|
||||
// normalize WordPress core report data and map into Status_Model
|
||||
$status->core = self::normalize_core_information( isset( $report_data->core ) ? $report_data->core : new \stdClass() );
|
||||
|
||||
// check if any installed items (themes, plugins, or core) have not been checked in the report
|
||||
$all_items = array_merge( $status->plugins, $status->themes, array( $status->core ) );
|
||||
$unchecked_items = array_filter(
|
||||
$all_items,
|
||||
function ( $item ) {
|
||||
return ! isset( $item->checked ) || ! $item->checked;
|
||||
}
|
||||
);
|
||||
$status->has_unchecked_items = ! empty( $unchecked_items );
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the list of installed extensions with the list of extensions that were checked for known vulnerabilities and return a normalized list to be used in the UI
|
||||
*
|
||||
* @param array $installed The list of installed extensions, where each attribute key is the extension slug.
|
||||
* @param object $checked The list of checked extensions.
|
||||
* @param array $append Additional data to append to each result in the list.
|
||||
* @return array Normalized list of extensions.
|
||||
*/
|
||||
protected static function merge_installed_and_checked_lists( $installed, $checked, $append ) {
|
||||
$new_list = array();
|
||||
foreach ( array_keys( $installed ) as $slug ) {
|
||||
|
||||
$checked = (object) $checked;
|
||||
|
||||
$extension = new Extension_Model(
|
||||
array_merge(
|
||||
array(
|
||||
'name' => $installed[ $slug ]['Name'],
|
||||
'version' => $installed[ $slug ]['Version'],
|
||||
'slug' => $slug,
|
||||
'threats' => array(),
|
||||
'checked' => false,
|
||||
),
|
||||
$append
|
||||
)
|
||||
);
|
||||
|
||||
if ( isset( $checked->{ $slug } ) && $checked->{ $slug }->version === $installed[ $slug ]['Version'] ) {
|
||||
$extension->version = $checked->{ $slug }->version;
|
||||
$extension->checked = true;
|
||||
|
||||
if ( is_array( $checked->{ $slug }->vulnerabilities ) ) {
|
||||
foreach ( $checked->{ $slug }->vulnerabilities as $threat ) {
|
||||
$extension->threats[] = new Threat_Model(
|
||||
array(
|
||||
'id' => $threat->id,
|
||||
'title' => $threat->title,
|
||||
'fixed_in' => $threat->fixed_in,
|
||||
'description' => isset( $threat->description ) ? $threat->description : null,
|
||||
'source' => isset( $threat->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $threat->id ) ) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new_list[] = $extension;
|
||||
|
||||
}
|
||||
|
||||
$new_list = parent::sort_threats( $new_list );
|
||||
|
||||
return $new_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WordPress version that was checked matches the current installed version.
|
||||
*
|
||||
* @param object $core_check The object returned by Protect wpcom endpoint.
|
||||
* @return object The object representing the current status of core checks.
|
||||
*/
|
||||
protected static function normalize_core_information( $core_check ) {
|
||||
global $wp_version;
|
||||
|
||||
$core = new Extension_Model(
|
||||
array(
|
||||
'type' => 'core',
|
||||
'name' => 'WordPress',
|
||||
'version' => $wp_version,
|
||||
'checked' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( isset( $core_check->version ) && $core_check->version === $wp_version ) {
|
||||
if ( is_array( $core_check->vulnerabilities ) ) {
|
||||
$core->checked = true;
|
||||
$core->set_threats(
|
||||
array_map(
|
||||
function ( $vulnerability ) {
|
||||
$vulnerability->source = isset( $vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $vulnerability->id ) ) : null;
|
||||
return $vulnerability;
|
||||
},
|
||||
$core_check->vulnerabilities
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $core;
|
||||
}
|
||||
}
|
@ -0,0 +1,427 @@
|
||||
<?php
|
||||
/**
|
||||
* Class file for managing REST API endpoints for Jetpack Protect.
|
||||
*
|
||||
* @since 1.2.2
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
|
||||
use Automattic\Jetpack\Waf\Waf_Runner;
|
||||
use Jetpack_Protect;
|
||||
use WP_Error;
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* Class REST_Controller
|
||||
*/
|
||||
class REST_Controller {
|
||||
|
||||
/**
|
||||
* Initialize the plugin's REST API.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
// Set up the REST authentication hooks.
|
||||
Connection_Rest_Authentication::init();
|
||||
|
||||
// Add custom WP REST API endoints.
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_rest_endpoints() {
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'check-plan',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_check_plan',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'status',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_get_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'clear-scan-cache',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_clear_scan_cache',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'ignore-threat',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_ignore_threat',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'fix-threats',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_fix_threats',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'fix-threats-status',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_fix_threats_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'check-credentials',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_check_credentials',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'scan',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_scan',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'toggle-waf',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_toggle_waf',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'waf',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_get_waf',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'waf-seen',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_get_waf_seen_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'waf-seen',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_set_waf_seen_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'waf-upgrade-seen',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => __CLASS__ . '::api_get_waf_upgrade_seen_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
'jetpack-protect/v1',
|
||||
'waf-upgrade-seen',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::api_set_waf_upgrade_seen_status',
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return site plan data for the API endpoint
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_check_plan() {
|
||||
$has_required_plan = Plan::has_required_plan();
|
||||
|
||||
return rest_ensure_response( $has_required_plan, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Protect Status for the API endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_get_status( $request ) {
|
||||
$status = Status::get_status( $request['hard_refresh'] );
|
||||
return rest_ensure_response( $status, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the Scan_Status cache for the API endpoint
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_clear_scan_cache() {
|
||||
$cache_cleared = Scan_Status::delete_option();
|
||||
|
||||
if ( ! $cache_cleared ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to clear the Jetpack Scan cache.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( 'Jetpack Scan cache cleared.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignores a threat for the API endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_ignore_threat( $request ) {
|
||||
if ( ! $request['threat_id'] ) {
|
||||
return new WP_REST_Response( 'Missing threat ID.', 400 );
|
||||
}
|
||||
|
||||
$threat_ignored = Threats::ignore_threat( $request['threat_id'] );
|
||||
|
||||
if ( ! $threat_ignored ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to ignore the threat.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( 'Threat ignored.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes threats for the API endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_fix_threats( $request ) {
|
||||
if ( empty( $request['threat_ids'] ) ) {
|
||||
return new WP_REST_Response( 'Missing threat IDs.', 400 );
|
||||
}
|
||||
|
||||
$threats_fixed = Threats::fix_threats( $request['threat_ids'] );
|
||||
|
||||
if ( ! $threats_fixed ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to fix the threat.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $threats_fixed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes threats for the API endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_fix_threats_status( $request ) {
|
||||
if ( empty( $request['threat_ids'] ) ) {
|
||||
return new WP_REST_Response( 'Missing threat IDs.', 400 );
|
||||
}
|
||||
|
||||
$threats_fixed = Threats::fix_threats_status( $request['threat_ids'] );
|
||||
|
||||
if ( ! $threats_fixed ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to get the fixer status of the threats.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $threats_fixed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the site has credentials configured
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_check_credentials() {
|
||||
$credential_array = Credentials::get_credential_array();
|
||||
|
||||
if ( ! isset( $credential_array ) ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to fetch the credentials array', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $credential_array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a scan for the API endpoint
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function api_scan() {
|
||||
$scan_enqueued = Threats::scan();
|
||||
|
||||
if ( ! $scan_enqueued ) {
|
||||
return new WP_REST_Response( 'An error occured while attempting to enqueue the scan.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( 'Scan enqueued.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the WAF module on or off for the API endpoint
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function api_toggle_waf() {
|
||||
if ( Waf_Runner::is_enabled() ) {
|
||||
$disabled = Waf_Runner::disable();
|
||||
if ( ! $disabled ) {
|
||||
return new WP_Error(
|
||||
'waf_disable_failed',
|
||||
__( 'An error occured disabling the firewall.', 'jetpack-protect' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( true );
|
||||
}
|
||||
|
||||
$enabled = Waf_Runner::enable();
|
||||
if ( ! $enabled ) {
|
||||
return new WP_Error(
|
||||
'waf_enable_failed',
|
||||
__( 'An error occured enabling the firewall.', 'jetpack-protect' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF data for the API endpoint
|
||||
*
|
||||
* @return WP_Rest_Response
|
||||
*/
|
||||
public static function api_get_waf() {
|
||||
// Ensure plugin activation has been performed so WAF module is available.
|
||||
Jetpack_Protect::do_plugin_activation_activities();
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'is_seen' => Jetpack_Protect::get_waf_seen_status(),
|
||||
'is_enabled' => Waf_Runner::is_enabled(),
|
||||
'config' => Waf_Runner::get_config(),
|
||||
'stats' => Jetpack_Protect::get_waf_stats(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF "Seen" status for the API endpoint
|
||||
*
|
||||
* @return bool Whether the current user has viewed the WAF screen.
|
||||
*/
|
||||
public static function api_get_waf_seen_status() {
|
||||
return Jetpack_Protect::get_waf_seen_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WAF "Seen" status for the API endpoint
|
||||
*
|
||||
* @return bool True if seen status updated to true, false on failure.
|
||||
*/
|
||||
public static function api_set_waf_seen_status() {
|
||||
return Jetpack_Protect::set_waf_seen_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF Upgrade "Seen" Status for the API endpoint
|
||||
*
|
||||
* @return bool Whether the current user has dismissed the upgrade popover or enabled the automatic rules feature.
|
||||
*/
|
||||
public static function api_get_waf_upgrade_seen_status() {
|
||||
return Jetpack_Protect::get_waf_upgrade_seen_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WAF Upgrade "Seen" Status for the API endpoint
|
||||
*
|
||||
* @return bool True if upgrade seen status updated to true, false on failure.
|
||||
*/
|
||||
public static function api_set_waf_upgrade_seen_status() {
|
||||
return Jetpack_Protect::set_waf_upgrade_seen_status();
|
||||
}
|
||||
}
|
@ -0,0 +1,359 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle the Scan Status of Jetpack Protect
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Plugins_Installer;
|
||||
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class that handles fetching of threats from the Scan API
|
||||
*/
|
||||
class Scan_Status extends Status {
|
||||
|
||||
/**
|
||||
* Scan endpoint
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCAN_API_BASE = '/sites/%d/scan';
|
||||
|
||||
/**
|
||||
* Name of the option where status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'jetpack_scan_status';
|
||||
|
||||
/**
|
||||
* Name of the option where the timestamp of the status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_TIMESTAMP_NAME = 'jetpack_scan_status_timestamp';
|
||||
|
||||
/**
|
||||
* Time in seconds that the cache should last
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const OPTION_EXPIRES_AFTER = 300; // 5 minutes.
|
||||
|
||||
/**
|
||||
* Gets the current status of the Jetpack Protect checks
|
||||
*
|
||||
* @param bool $refresh_from_wpcom Refresh the local plan and status cache from wpcom.
|
||||
* @return Status_Model
|
||||
*/
|
||||
public static function get_status( $refresh_from_wpcom = false ) {
|
||||
if ( self::$status !== null ) {
|
||||
return self::$status;
|
||||
}
|
||||
|
||||
if ( $refresh_from_wpcom || ! self::should_use_cache() || self::is_cache_expired() ) {
|
||||
$status = self::fetch_from_api();
|
||||
} else {
|
||||
$status = self::get_from_options();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $status ) ) {
|
||||
$status = new Status_Model(
|
||||
array(
|
||||
'error' => true,
|
||||
'error_code' => $status->get_error_code(),
|
||||
'error_message' => $status->get_error_message(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$status = self::normalize_api_data( $status );
|
||||
}
|
||||
|
||||
self::$status = $status;
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Scan API endpoint
|
||||
*
|
||||
* @return WP_Error|string
|
||||
*/
|
||||
public static function get_api_url() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
$is_connected = ( new Connection_Manager() )->is_connected();
|
||||
|
||||
if ( ! $blog_id || ! $is_connected ) {
|
||||
return new WP_Error( 'site_not_connected' );
|
||||
}
|
||||
|
||||
$api_url = sprintf( self::SCAN_API_BASE, $blog_id );
|
||||
|
||||
return $api_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the status data from the Scan API
|
||||
*
|
||||
* @return WP_Error|array
|
||||
*/
|
||||
public static function fetch_from_api() {
|
||||
$api_url = self::get_api_url();
|
||||
if ( is_wp_error( $api_url ) ) {
|
||||
return $api_url;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
self::get_api_url(),
|
||||
'2',
|
||||
array( 'method' => 'GET' ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) {
|
||||
return new WP_Error( 'failed_fetching_status', 'Failed to fetch Scan data from the server', array( 'status' => $response_code ) );
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
self::update_status_option( $body );
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize API Data
|
||||
* Formats the payload from the Scan API into an instance of Status_Model.
|
||||
*
|
||||
* @param object $scan_data The data returned by the scan API.
|
||||
*
|
||||
* @return Status_Model
|
||||
*/
|
||||
private static function normalize_api_data( $scan_data ) {
|
||||
global $wp_version;
|
||||
|
||||
$status = new Status_Model();
|
||||
$status->data_source = 'scan_api';
|
||||
$status->status = isset( $scan_data->state ) ? $scan_data->state : null;
|
||||
$status->num_threats = 0;
|
||||
$status->num_themes_threats = 0;
|
||||
$status->num_plugins_threats = 0;
|
||||
$status->has_unchecked_items = false;
|
||||
$status->current_progress = isset( $scan_data->current->progress ) ? $scan_data->current->progress : null;
|
||||
|
||||
if ( ! empty( $scan_data->most_recent->timestamp ) ) {
|
||||
$date = new \DateTime( $scan_data->most_recent->timestamp );
|
||||
if ( $date ) {
|
||||
$status->last_checked = $date->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
}
|
||||
|
||||
$status->core = new Extension_Model(
|
||||
array(
|
||||
'type' => 'core',
|
||||
'name' => 'WordPress',
|
||||
'version' => $wp_version,
|
||||
'checked' => true, // to do: default to false once Scan API has manifest
|
||||
)
|
||||
);
|
||||
|
||||
if ( isset( $scan_data->threats ) && is_array( $scan_data->threats ) ) {
|
||||
foreach ( $scan_data->threats as $threat ) {
|
||||
if ( isset( $threat->extension->type ) ) {
|
||||
if ( 'plugin' === $threat->extension->type ) {
|
||||
// add the extension if it does not yet exist in the status
|
||||
if ( ! isset( $status->plugins[ $threat->extension->slug ] ) ) {
|
||||
$status->plugins[ $threat->extension->slug ] = new Extension_Model(
|
||||
array(
|
||||
'name' => isset( $threat->extension->name ) ? $threat->extension->name : null,
|
||||
'slug' => isset( $threat->extension->slug ) ? $threat->extension->slug : null,
|
||||
'version' => isset( $threat->extension->version ) ? $threat->extension->version : null,
|
||||
'type' => 'plugin',
|
||||
'checked' => true,
|
||||
'threats' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$status->plugins[ $threat->extension->slug ]->threats[] = new Threat_Model(
|
||||
array(
|
||||
'id' => isset( $threat->id ) ? $threat->id : null,
|
||||
'signature' => isset( $threat->signature ) ? $threat->signature : null,
|
||||
'title' => isset( $threat->title ) ? $threat->title : null,
|
||||
'description' => isset( $threat->description ) ? $threat->description : null,
|
||||
'vulnerability_description' => isset( $threat->vulnerability_description ) ? $threat->vulnerability_description : null,
|
||||
'fix_description' => isset( $threat->fix_description ) ? $threat->fix_description : null,
|
||||
'payload_subtitle' => isset( $threat->payload_subtitle ) ? $threat->payload_subtitle : null,
|
||||
'payload_description' => isset( $threat->payload_description ) ? $threat->payload_description : null,
|
||||
'first_detected' => isset( $threat->first_detected ) ? $threat->first_detected : null,
|
||||
'fixed_in' => isset( $threat->fixer->fixer ) && 'update' === $threat->fixer->fixer ? $threat->fixer->target : null,
|
||||
'severity' => isset( $threat->severity ) ? $threat->severity : null,
|
||||
'fixable' => isset( $threat->fixer ) ? $threat->fixer : null,
|
||||
'status' => isset( $threat->status ) ? $threat->status : null,
|
||||
'filename' => isset( $threat->filename ) ? $threat->filename : null,
|
||||
'context' => isset( $threat->context ) ? $threat->context : null,
|
||||
'source' => isset( $threat->source ) ? $threat->source : null,
|
||||
)
|
||||
);
|
||||
++$status->num_threats;
|
||||
++$status->num_plugins_threats;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'theme' === $threat->extension->type ) {
|
||||
// add the extension if it does not yet exist in the status
|
||||
if ( ! isset( $status->themes[ $threat->extension->slug ] ) ) {
|
||||
$status->themes[ $threat->extension->slug ] = new Extension_Model(
|
||||
array(
|
||||
'name' => isset( $threat->extension->name ) ? $threat->extension->name : null,
|
||||
'slug' => isset( $threat->extension->slug ) ? $threat->extension->slug : null,
|
||||
'version' => isset( $threat->extension->version ) ? $threat->extension->version : null,
|
||||
'type' => 'theme',
|
||||
'checked' => true,
|
||||
'threats' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$status->themes[ $threat->extension->slug ]->threats[] = new Threat_Model(
|
||||
array(
|
||||
'id' => isset( $threat->id ) ? $threat->id : null,
|
||||
'signature' => isset( $threat->signature ) ? $threat->signature : null,
|
||||
'title' => isset( $threat->title ) ? $threat->title : null,
|
||||
'description' => isset( $threat->description ) ? $threat->description : null,
|
||||
'vulnerability_description' => isset( $threat->vulnerability_description ) ? $threat->vulnerability_description : null,
|
||||
'fix_description' => isset( $threat->fix_description ) ? $threat->fix_description : null,
|
||||
'payload_subtitle' => isset( $threat->payload_subtitle ) ? $threat->payload_subtitle : null,
|
||||
'payload_description' => isset( $threat->payload_description ) ? $threat->payload_description : null,
|
||||
'first_detected' => isset( $threat->first_detected ) ? $threat->first_detected : null,
|
||||
'fixed_in' => isset( $threat->fixer->fixer ) && 'update' === $threat->fixer->fixer ? $threat->fixer->target : null,
|
||||
'severity' => isset( $threat->severity ) ? $threat->severity : null,
|
||||
'fixable' => isset( $threat->fixer ) ? $threat->fixer : null,
|
||||
'status' => isset( $threat->status ) ? $threat->status : null,
|
||||
'filename' => isset( $threat->filename ) ? $threat->filename : null,
|
||||
'context' => isset( $threat->context ) ? $threat->context : null,
|
||||
'source' => isset( $threat->source ) ? $threat->source : null,
|
||||
)
|
||||
);
|
||||
++$status->num_threats;
|
||||
++$status->num_themes_threats;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $threat->signature ) && 'Vulnerable.WP.Core' === $threat->signature ) {
|
||||
if ( $threat->version !== $wp_version ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status->core->threats[] = new Threat_Model(
|
||||
array(
|
||||
'id' => $threat->id,
|
||||
'signature' => $threat->signature,
|
||||
'title' => $threat->title,
|
||||
'description' => $threat->description,
|
||||
'first_detected' => $threat->first_detected,
|
||||
'severity' => $threat->severity,
|
||||
)
|
||||
);
|
||||
++$status->num_threats;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $threat->filename ) ) {
|
||||
$status->files[] = new Threat_Model( $threat );
|
||||
++$status->num_threats;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $threat->table ) ) {
|
||||
$status->database[] = new Threat_Model( $threat );
|
||||
++$status->num_threats;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$installed_plugins = Plugins_Installer::get_plugins();
|
||||
$status->plugins = self::merge_installed_and_checked_lists( $installed_plugins, $status->plugins, array( 'type' => 'plugins' ), true );
|
||||
|
||||
$installed_themes = Sync_Functions::get_themes();
|
||||
$status->themes = self::merge_installed_and_checked_lists( $installed_themes, $status->themes, array( 'type' => 'themes' ), true );
|
||||
|
||||
foreach ( array_merge( $status->themes, $status->plugins ) as $extension ) {
|
||||
if ( ! $extension->checked ) {
|
||||
$status->has_unchecked_items = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the list of installed extensions with the list of extensions that were checked for known vulnerabilities and return a normalized list to be used in the UI
|
||||
*
|
||||
* @param array $installed The list of installed extensions, where each attribute key is the extension slug.
|
||||
* @param object $checked The list of checked extensions.
|
||||
* @param array $append Additional data to append to each result in the list.
|
||||
* @return array Normalized list of extensions.
|
||||
*/
|
||||
protected static function merge_installed_and_checked_lists( $installed, $checked, $append ) {
|
||||
$new_list = array();
|
||||
$checked = (object) $checked;
|
||||
|
||||
foreach ( array_keys( $installed ) as $slug ) {
|
||||
/**
|
||||
* Extension Type Map
|
||||
*
|
||||
* @var array $extension_type_map Key value pairs of extension types and their corresponding
|
||||
* identifier used by the Scan API data source.
|
||||
*/
|
||||
$extension_type_map = array(
|
||||
'themes' => 'r1',
|
||||
'plugins' => 'r2',
|
||||
);
|
||||
|
||||
$version = $installed[ $slug ]['Version'];
|
||||
$short_slug = str_replace( '.php', '', explode( '/', $slug )[0] );
|
||||
$scanifest_slug = $extension_type_map[ $append['type'] ] . ":$short_slug@$version";
|
||||
|
||||
$extension = new Extension_Model(
|
||||
array_merge(
|
||||
array(
|
||||
'name' => $installed[ $slug ]['Name'],
|
||||
'version' => $version,
|
||||
'slug' => $slug,
|
||||
'threats' => array(),
|
||||
'checked' => false,
|
||||
),
|
||||
$append
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! isset( $checked->extensions ) // no extension data available from Scan API
|
||||
|| is_array( $checked->extensions ) && in_array( $scanifest_slug, $checked->extensions, true ) // extension data matches Scan API
|
||||
) {
|
||||
$extension->checked = true;
|
||||
if ( isset( $checked->{ $short_slug }->threats ) ) {
|
||||
$extension->threats = $checked->{ $short_slug }->threats;
|
||||
}
|
||||
}
|
||||
|
||||
$new_list[] = $extension;
|
||||
|
||||
}
|
||||
|
||||
$new_list = parent::sort_threats( $new_list );
|
||||
|
||||
return $new_list;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle the Check in the Site Health admin page
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
/**
|
||||
* Site_Health.
|
||||
*
|
||||
* Displays threats in WordPress site health page.
|
||||
*/
|
||||
class Site_Health {
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! has_filter( 'site_status_tests', array( __CLASS__, 'add_check' ) ) ) {
|
||||
add_filter( 'site_status_tests', array( __CLASS__, 'add_check' ), 99 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add site-health page tests.
|
||||
*
|
||||
* @param array $checks Core checks.
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function add_check( $checks ) {
|
||||
$checks['direct']['jetpack_protect_checks'] = array(
|
||||
'label' => __( 'Jetpack Protect checks', 'jetpack-protect' ),
|
||||
'test' => array( __CLASS__, 'do_checks' ),
|
||||
);
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do site-health page checks
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function do_checks() {
|
||||
$total_threats = Status::get_total_threats();
|
||||
$threats = Status::get_all_threats();
|
||||
$threats = array_map(
|
||||
function ( $v ) {
|
||||
return $v->title;
|
||||
},
|
||||
$threats
|
||||
);
|
||||
|
||||
/**
|
||||
* Default, no threats found
|
||||
*/
|
||||
$result = array(
|
||||
'label' => __( 'No known threats found', 'jetpack-protect' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => __( 'Security', 'jetpack-protect' ),
|
||||
'color' => 'gray',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'Jetpack Protect did not find any known threats in your site. Threats can be exploited by hackers and cause harm to your website.', 'jetpack-protect' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'jetpack_protect_checks',
|
||||
);
|
||||
|
||||
/**
|
||||
* If threats found.
|
||||
*/
|
||||
if ( $total_threats ) {
|
||||
$result['status'] = 'critical';
|
||||
/* translators: $d is the number of threats found. */
|
||||
$result['label'] = sprintf( _n( 'Your site is affected by %d security threat', 'Your site is affected by %d security threats', $total_threats, 'jetpack-protect' ), $total_threats );
|
||||
$result['description'] = __( 'Jetpack Protect detected the following security threats in your site:', 'jetpack-protect' );
|
||||
|
||||
foreach ( $threats as $threat ) {
|
||||
$result['description'] .= '<p>';
|
||||
$result['description'] .= "<span class='dashicons dashicons-warning' style='color: crimson;'></span>  ";
|
||||
$result['description'] .= wp_kses( $threat, array( 'a' => array( 'href' => array() ) ) ); // Only allow a href HTML tags.
|
||||
$result['description'] .= '</p>';
|
||||
}
|
||||
$result['description'] .= '<p>';
|
||||
$result['description'] .= sprintf(
|
||||
wp_kses(
|
||||
/* translators: Link to Jetpack Protect. */
|
||||
__( 'See <a href="%s">Protect overview page</a> for more information.', 'jetpack-protect' ),
|
||||
array(
|
||||
'a' => array( 'href' => array() ),
|
||||
)
|
||||
),
|
||||
esc_url( admin_url( 'admin.php?page=jetpack-protect' ) )
|
||||
);
|
||||
$result['description'] .= '</p>';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle the Status of Jetpack Protect
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
/**
|
||||
* Class that handles fetching and caching the Status of vulnerabilities check from the WPCOM servers
|
||||
*/
|
||||
class Status {
|
||||
/**
|
||||
* Name of the option where status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = '';
|
||||
|
||||
/**
|
||||
* Name of the option where the timestamp of the status is stored
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_TIMESTAMP_NAME = '';
|
||||
|
||||
/**
|
||||
* Time in seconds that the cache should last
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const OPTION_EXPIRES_AFTER = 3600; // 1 hour.
|
||||
|
||||
/**
|
||||
* Time in seconds that the cache for the initial empty response should last
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const INITIAL_OPTION_EXPIRES_AFTER = 1 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Memoization for the current status
|
||||
*
|
||||
* @var null|Status_Model
|
||||
*/
|
||||
public static $status = null;
|
||||
|
||||
/**
|
||||
* Gets the current status of the Jetpack Protect checks
|
||||
*
|
||||
* @param bool $refresh_from_wpcom Refresh the local plan and status cache from wpcom.
|
||||
* @return Status_Model
|
||||
*/
|
||||
public static function get_status( $refresh_from_wpcom = false ) {
|
||||
$use_scan_status = Plan::has_required_plan();
|
||||
|
||||
if ( defined( 'JETPACK_PROTECT_DEV__DATA_SOURCE' ) ) {
|
||||
if ( 'scan_api' === JETPACK_PROTECT_DEV__DATA_SOURCE ) {
|
||||
$use_scan_status = true;
|
||||
}
|
||||
|
||||
if ( 'protect_report' === JETPACK_PROTECT_DEV__DATA_SOURCE ) {
|
||||
$use_scan_status = false;
|
||||
}
|
||||
}
|
||||
|
||||
self::$status = $use_scan_status ? Scan_Status::get_status( $refresh_from_wpcom ) : Protect_Status::get_status( $refresh_from_wpcom );
|
||||
return self::$status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current cached status is expired and should be renewed
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_cache_expired() {
|
||||
$option_timestamp = get_option( static::OPTION_TIMESTAMP_NAME );
|
||||
|
||||
if ( ! $option_timestamp ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() > (int) $option_timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should consider the stored cache or bypass it
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_use_cache() {
|
||||
return defined( 'JETPACK_PROTECT_DEV__BYPASS_CACHE' ) && JETPACK_PROTECT_DEV__BYPASS_CACHE ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current cached status
|
||||
*
|
||||
* @return bool|array False if value is not found. Array with values if cache is found.
|
||||
*/
|
||||
public static function get_from_options() {
|
||||
return maybe_unserialize( get_option( static::OPTION_NAME ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated the cached status and its timestamp
|
||||
*
|
||||
* @param array $status The new status to be cached.
|
||||
* @return void
|
||||
*/
|
||||
public static function update_status_option( $status ) {
|
||||
// TODO: Sanitize $status.
|
||||
update_option( static::OPTION_NAME, maybe_serialize( $status ) );
|
||||
$end_date = self::get_cache_end_date_by_status( $status );
|
||||
update_option( static::OPTION_TIMESTAMP_NAME, $end_date );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp the cache should expire depending on the current status
|
||||
*
|
||||
* Initial empty status, which are returned before the first check was performed, should be cache for less time
|
||||
*
|
||||
* @param object $status The response from the server being cached.
|
||||
* @return int The timestamp when the cache should expire.
|
||||
*/
|
||||
public static function get_cache_end_date_by_status( $status ) {
|
||||
if ( ! is_object( $status ) || empty( $status->last_checked ) ) {
|
||||
return time() + static::INITIAL_OPTION_EXPIRES_AFTER;
|
||||
}
|
||||
return time() + static::OPTION_EXPIRES_AFTER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cached status and its timestamp
|
||||
*
|
||||
* @return bool Whether all related status options were successfully deleted.
|
||||
*/
|
||||
public static function delete_option() {
|
||||
$option_deleted = delete_option( static::OPTION_NAME );
|
||||
$option_timestamp_deleted = delete_option( static::OPTION_TIMESTAMP_NAME );
|
||||
|
||||
return $option_deleted && $option_timestamp_deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current status to see if there are any threats found
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has_threats() {
|
||||
return 0 < self::get_total_threats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of threats found
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function get_total_threats() {
|
||||
$status = static::get_status();
|
||||
return isset( $status->num_threats ) && is_int( $status->num_threats ) ? $status->num_threats : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all threats combined
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all_threats() {
|
||||
return array_merge(
|
||||
self::get_wordpress_threats(),
|
||||
self::get_themes_threats(),
|
||||
self::get_plugins_threats(),
|
||||
self::get_files_threats(),
|
||||
self::get_database_threats()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threats found for WordPress core
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_wordpress_threats() {
|
||||
return self::get_threats( 'core' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threats found for themes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_themes_threats() {
|
||||
return self::get_threats( 'themes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threats found for plugins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_plugins_threats() {
|
||||
return self::get_threats( 'plugins' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threats found for files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_files_threats() {
|
||||
return self::get_threats( 'files' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threats found for plugins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_database_threats() {
|
||||
return self::get_threats( 'database' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the threats for one type of extension or core
|
||||
*
|
||||
* @param string $type What threats you want to get. Possible values are 'core', 'themes' and 'plugins'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_threats( $type ) {
|
||||
$status = static::get_status();
|
||||
|
||||
if ( 'core' === $type ) {
|
||||
return isset( $status->$type ) && ! empty( $status->$type->threats ) ? $status->$type->threats : array();
|
||||
}
|
||||
|
||||
if ( 'files' === $type || 'database' === $type ) {
|
||||
return isset( $status->$type ) && ! empty( $status->$type ) ? $status->$type : array();
|
||||
}
|
||||
|
||||
$threats = array();
|
||||
if ( isset( $status->$type ) ) {
|
||||
foreach ( (array) $status->$type as $item ) {
|
||||
if ( ! empty( $item->threats ) ) {
|
||||
$threats = array_merge( $threats, $item->threats );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $threats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WordPress version that was checked matches the current installed version.
|
||||
*
|
||||
* @param object $core_check The object returned by Protect wpcom endpoint.
|
||||
* @return object The object representing the current status of core checks.
|
||||
*/
|
||||
protected static function normalize_core_information( $core_check ) {
|
||||
global $wp_version;
|
||||
|
||||
$core = new Extension_Model(
|
||||
array(
|
||||
'type' => 'core',
|
||||
'name' => 'WordPress',
|
||||
'version' => $wp_version,
|
||||
'checked' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( isset( $core_check->version ) && $core_check->version === $wp_version ) {
|
||||
if ( is_array( $core_check->vulnerabilities ) ) {
|
||||
$core->checked = true;
|
||||
$core->set_threats( $core_check->vulnerabilities );
|
||||
}
|
||||
}
|
||||
|
||||
return $core;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort By Threats
|
||||
*
|
||||
* @param array<object> $threats Array of threats to sort.
|
||||
*
|
||||
* @return array<object> The sorted $threats array.
|
||||
*/
|
||||
protected static function sort_threats( $threats ) {
|
||||
usort(
|
||||
$threats,
|
||||
function ( $a, $b ) {
|
||||
// sort primarily based on the presence of threats
|
||||
if ( ! empty( $a->threats ) && empty( $b->threats ) ) {
|
||||
return -1;
|
||||
}
|
||||
if ( empty( $a->threats ) && ! empty( $b->threats ) ) {
|
||||
return 1;
|
||||
}
|
||||
// sort secondarily on whether the item has been checked
|
||||
if ( $a->checked && ! $b->checked ) {
|
||||
return 1;
|
||||
}
|
||||
if ( ! $a->checked && $b->checked ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
return $threats;
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Class to handle Threats
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class that handles management of individual threats.
|
||||
*/
|
||||
class Threats {
|
||||
/**
|
||||
* Gets the base "Alerts" (Threats) endpoint.
|
||||
*
|
||||
* @return WP_Error|string
|
||||
*/
|
||||
private static function get_api_base() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
$is_connected = ( new Connection_Manager() )->is_connected();
|
||||
|
||||
if ( ! $blog_id || ! $is_connected ) {
|
||||
return new WP_Error( 'site_not_connected' );
|
||||
}
|
||||
|
||||
$api_url = sprintf( '/sites/%d/alerts', $blog_id );
|
||||
|
||||
return $api_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Threat
|
||||
*
|
||||
* @param string $threat_id The threat ID.
|
||||
* @param array $updates The keys/values to update.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_threat( $threat_id, $updates ) {
|
||||
$api_base = self::get_api_base( $threat_id );
|
||||
if ( is_wp_error( $api_base ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
"$api_base/$threat_id",
|
||||
'2',
|
||||
array( 'method' => 'POST' ),
|
||||
wp_json_encode( $updates ),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// clear the now out-of-date cache
|
||||
Scan_Status::delete_option();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore Threat
|
||||
*
|
||||
* @param string $threat_id The threat ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function ignore_threat( $threat_id ) {
|
||||
return self::update_threat( $threat_id, array( 'ignore' => true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix Threats
|
||||
*
|
||||
* @param array<string> $threat_ids Threat IDs.
|
||||
*
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function fix_threats( $threat_ids ) {
|
||||
$api_base = self::get_api_base();
|
||||
if ( is_wp_error( $api_base ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
"$api_base/fix",
|
||||
'2',
|
||||
array( 'method' => 'POST' ),
|
||||
wp_json_encode(
|
||||
array(
|
||||
'threat_ids' => $threat_ids,
|
||||
)
|
||||
),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// clear the now out-of-date cache
|
||||
Scan_Status::delete_option();
|
||||
|
||||
$parsed_response = json_decode( $response['body'] );
|
||||
|
||||
if ( ! $parsed_response ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $parsed_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix Threats Status
|
||||
*
|
||||
* @param array<string> $threat_ids Threat IDs.
|
||||
*
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function fix_threats_status( $threat_ids ) {
|
||||
$api_base = self::get_api_base();
|
||||
if ( is_wp_error( $api_base ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
add_query_arg( 'threat_ids', $threat_ids, "$api_base/fix" ),
|
||||
'2',
|
||||
array( 'method' => 'GET' ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parsed_response = json_decode( $response['body'] );
|
||||
|
||||
if ( ! $parsed_response ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// clear the potentially out-of-date cache
|
||||
Scan_Status::delete_option();
|
||||
|
||||
return $parsed_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan enqueue
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function scan() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
$is_connected = ( new Connection_Manager() )->is_connected();
|
||||
|
||||
if ( ! $blog_id || ! $is_connected ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api_base = sprintf( '/sites/%d/scan', $blog_id );
|
||||
|
||||
if ( is_wp_error( $api_base ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
"$api_base/enqueue",
|
||||
'2',
|
||||
array( 'method' => 'POST' ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== $response_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// clear the now out-of-date cache
|
||||
Scan_Status::delete_option();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Model class for extensions.
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
/**
|
||||
* Model class for extension data.
|
||||
*/
|
||||
class Extension_Model {
|
||||
|
||||
/**
|
||||
* The extension name.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The extension slug.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $slug;
|
||||
|
||||
/**
|
||||
* The extension version.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $version;
|
||||
|
||||
/**
|
||||
* A collection of threats related to this version of the extension.
|
||||
*
|
||||
* @var array<Threat_Model>
|
||||
*/
|
||||
public $threats = array();
|
||||
|
||||
/**
|
||||
* Whether the extension has been checked for threats.
|
||||
*
|
||||
* @var null|bool
|
||||
*/
|
||||
public $checked;
|
||||
|
||||
/**
|
||||
* The type of extension ("plugins", "themes", or "core").
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Extension Model Constructor
|
||||
*
|
||||
* @param array|object $extension Extension data to load into the model instance.
|
||||
*/
|
||||
public function __construct( $extension = array() ) {
|
||||
if ( is_object( $extension ) ) {
|
||||
$extension = (array) $extension;
|
||||
}
|
||||
|
||||
foreach ( $extension as $property => $value ) {
|
||||
if ( property_exists( $this, $property ) ) {
|
||||
// use the property's setter method when possible
|
||||
if ( method_exists( $this, "set_$property" ) ) {
|
||||
$this->{ "set_$property" }( $value );
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise, map the value directly into the class property
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Threats
|
||||
*
|
||||
* @param array<Threat_Model|array|object> $threats An array of threat data to add to the extension.
|
||||
*/
|
||||
public function set_threats( $threats ) {
|
||||
if ( ! is_array( $threats ) ) {
|
||||
$this->threats = array();
|
||||
return;
|
||||
}
|
||||
|
||||
// convert each provided threat item into an instance of Threat_Model
|
||||
$threats = array_map(
|
||||
function ( $threat ) {
|
||||
if ( is_a( $threat, 'Threat_Model' ) ) {
|
||||
return $threat;
|
||||
}
|
||||
|
||||
if ( is_object( $threat ) ) {
|
||||
$threat = (array) $threat;
|
||||
}
|
||||
|
||||
return new Threat_Model( $threat );
|
||||
},
|
||||
$threats
|
||||
);
|
||||
|
||||
$this->threats = $threats;
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* Model class for Protect status report data.
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
/**
|
||||
* Model class for the Protect status report data.
|
||||
*/
|
||||
class Status_Model {
|
||||
/**
|
||||
* Data source.
|
||||
*
|
||||
* @var string protect_report|scan_api
|
||||
*/
|
||||
public $data_source;
|
||||
|
||||
/**
|
||||
* The date and time when the status was generated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $last_checked;
|
||||
|
||||
/**
|
||||
* The number of threats.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $num_threats;
|
||||
|
||||
/**
|
||||
* The number of plugin threats.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $num_plugins_threats;
|
||||
|
||||
/**
|
||||
* The number of theme threats.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $num_themes_threats;
|
||||
|
||||
/**
|
||||
* The current report status.
|
||||
*
|
||||
* @var string in_progress|scheduled|idle|scanning|provisioning|unavailable
|
||||
*/
|
||||
public $status;
|
||||
|
||||
/**
|
||||
* WordPress core status.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
public $core;
|
||||
|
||||
/**
|
||||
* Status themes.
|
||||
*
|
||||
* @var array<Extension_Model>
|
||||
*/
|
||||
public $themes = array();
|
||||
|
||||
/**
|
||||
* Status plugins.
|
||||
*
|
||||
* @var array<Extension_Model>
|
||||
*/
|
||||
public $plugins = array();
|
||||
|
||||
/**
|
||||
* File threats.
|
||||
*
|
||||
* @var array<Extension_Model>
|
||||
*/
|
||||
public $files = array();
|
||||
|
||||
/**
|
||||
* Database threats.
|
||||
*
|
||||
* @var array<Extension_Model>
|
||||
*/
|
||||
public $database = array();
|
||||
|
||||
/**
|
||||
* Whether the site includes items that have not been checked.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $has_unchecked_items;
|
||||
|
||||
/**
|
||||
* The estimated percentage of the current scan.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $current_progress;
|
||||
|
||||
/**
|
||||
* Whether there was an error loading the status.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $error = false;
|
||||
|
||||
/**
|
||||
* The error code thrown when loading the status.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $error_code;
|
||||
|
||||
/**
|
||||
* The error message thrown when loading the status.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $error_message;
|
||||
|
||||
/**
|
||||
* Status constructor.
|
||||
*
|
||||
* @param array $status The status data to load into the class instance.
|
||||
*/
|
||||
public function __construct( $status = array() ) {
|
||||
// set status defaults
|
||||
$this->core = new \stdClass();
|
||||
|
||||
foreach ( $status as $property => $value ) {
|
||||
if ( property_exists( $this, $property ) ) {
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Model class for threat data.
|
||||
*
|
||||
* @package automattic/jetpack-protect-plugin
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Protect;
|
||||
|
||||
/**
|
||||
* Model class for threat data.
|
||||
*/
|
||||
class Threat_Model {
|
||||
|
||||
/**
|
||||
* Threat ID.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Threat Signature.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $signature;
|
||||
|
||||
/**
|
||||
* Threat Title.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* Threat Description.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* The data the threat was first detected.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $first_detected;
|
||||
|
||||
/**
|
||||
* The version the threat is fixed in.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $fixed_in;
|
||||
|
||||
/**
|
||||
* The severity of the threat between 1-5.
|
||||
*
|
||||
* @var null|int
|
||||
*/
|
||||
public $severity;
|
||||
|
||||
/**
|
||||
* Information about the auto-fix available for this threat. False when not auto-fixable.
|
||||
*
|
||||
* @var null|bool|object
|
||||
*/
|
||||
public $fixable;
|
||||
|
||||
/**
|
||||
* The current status of the threat.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $status;
|
||||
|
||||
/**
|
||||
* The filename of the threat.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $filename;
|
||||
|
||||
/**
|
||||
* The context of the threat.
|
||||
*
|
||||
* @var null|object
|
||||
*/
|
||||
public $context;
|
||||
|
||||
/**
|
||||
* The source URL of the threat.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* Threat Constructor
|
||||
*
|
||||
* @param array|object $threat Threat data to load into the class instance.
|
||||
*/
|
||||
public function __construct( $threat ) {
|
||||
if ( is_object( $threat ) ) {
|
||||
$threat = (array) $threat;
|
||||
}
|
||||
|
||||
foreach ( $threat as $property => $value ) {
|
||||
if ( property_exists( $this, $property ) ) {
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user