Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -6,10 +6,12 @@ class Controller_Permissions {
const CAP_ACTIVATE_2FA_SELF = 'wf2fa_activate_2fa_self'; //Activate 2FA on its own user account
const CAP_ACTIVATE_2FA_OTHERS = 'wf2fa_activate_2fa_others'; //Activate 2FA on user accounts other than its own
const CAP_MANAGE_SETTINGS = 'wf2fa_manage_settings'; //Edit settings for the plugin
const SITE_BATCH_SIZE = 50; //The maximum number of sites to process during a single request
const SETTING_LAST_ROLE_CHANGE = 'wfls_last_role_change';
const SETTING_LAST_ROLE_SYNC = 'wfls_last_role_sync';
private $network_roles = array();
private $multisite_roles = null;
/**
* Returns the singleton Controller_Permissions.
@@ -23,15 +25,9 @@ class Controller_Permissions {
}
return $_shared;
}
private function on_role_change() {
update_site_option('wfls_last_role_change', time());
if(is_multisite())
update_site_option('wfls_role_batch_position', 0);
}
public function install() {
$this->on_role_change();
$this->_on_role_change();
if (is_multisite()) {
//Super Admin automatically gets all capabilities, so we don't need to explicitly add them
$this->_add_cap_multisite('administrator', self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
@@ -42,57 +38,102 @@ class Controller_Permissions {
$this->_add_cap('administrator', self::CAP_MANAGE_SETTINGS);
}
}
public function uninstall() {
if (Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_DELETE_ON_DEACTIVATION)) {
if (is_multisite()) {
$sites = $this->get_sites();
foreach ($sites as $id) {
switch_to_blog($id);
wp_clear_scheduled_hook('wordfence_ls_role_sync_cron');
restore_current_blog();
}
}
}
}
public static function _init_actions() {
add_action('wordfence_ls_role_sync_cron', array(Controller_Permissions::shared(), '_role_sync_cron'));
}
public function init() {
global $wp_version;
if(is_multisite()){
if(version_compare($wp_version, '5.1.0', '>=')){
if (is_multisite()) {
if (version_compare($wp_version, '5.1.0', '>=')) {
add_action('wp_initialize_site', array($this, '_wp_initialize_site'), 99);
}
else{
else {
add_action('wpmu_new_blog', array($this, '_wpmu_new_blog'), 10, 5);
}
add_action('init', array($this, 'check_role_sync'), 1);
add_action('init', array($this, '_validate_role_sync_cron'), 1);
}
}
/**
* Syncs roles to the new multisite blog.
*
* @param $site_id
* @param $user_id
* @param $domain
* @param $path
* @param $network_id
*/
public function _wpmu_new_blog($site_id, $user_id, $domain, $path, $network_id) {
$this->sync_roles($network_id, $site_id);
}
/**
* Syncs roles to the new multisite blog.
*
* @param $new_site
*/
public function _wp_initialize_site($new_site) {
$this->sync_roles($new_site->site_id, $new_site->blog_id);
}
public function check_role_sync() {
//Trigger an initial update for existing installations
$last_role_change=(int)get_site_option('wfls_last_role_change', 0);
if($last_role_change===0)
$this->on_role_change();
//Process the current batch if necessary
$position=(int)get_site_option('wfls_role_batch_position', 0);
if($position===-1)
return;
$sites=$this->get_sites($position, self::SITE_BATCH_SIZE);
if(empty($sites)){
$position=-1;
return;
/**
* Creates the hourly cron (if needed) that handles syncing the roles/permissions for the current blog. Because crons
* are specific to individual blogs on multisite rather than to the network itself, this will end up creating a cron
* for every member blog of the multisite.
*
* If there is a new role change since the last sync, a one-off cron will be fired to sync it sooner than the normal
* recurrence period.
*
* Multisite only.
*
*/
public function _validate_role_sync_cron() {
if (!wp_next_scheduled('wordfence_ls_role_sync_cron')) {
wp_schedule_event(time(), 'hourly', 'wordfence_ls_role_sync_cron');
}
else{
$network_id=get_current_site()->id;
foreach($sites as $site){
$site=(int)$site;
$this->sync_roles($network_id, $site);
else {
$last_role_change = (int) get_site_option(self::SETTING_LAST_ROLE_CHANGE, 0);
if ($last_role_change >= get_option(self::SETTING_LAST_ROLE_SYNC, 0)) {
wp_schedule_single_event(time(), 'wordfence_ls_role_sync_cron'); //Force queue an update in case the normal cron is still a while out
}
$position=$site;
}
update_site_option('wfls_role_batch_position', $position);
//Update the current site if not already up to date
$site_id=get_current_blog_id();
if($last_role_change>=get_option('wfls_last_role_sync', 0)&&$site_id>=$position){
$this->sync_roles(get_current_site()->id, $site_id);
update_option('wfls_last_role_sync', time());
}
/**
* Handles syncing the roles/permissions for the current blog when the cron fires.
*/
public function _role_sync_cron() {
$last_role_change = (int) get_site_option(self::SETTING_LAST_ROLE_CHANGE, 0);
if ($last_role_change === 0) {
$this->_on_role_change();
}
if ($last_role_change >= get_option(self::SETTING_LAST_ROLE_SYNC, 0)) {
$network_id = get_current_site()->id;
$blog_id = get_current_blog_id();
$this->sync_roles($network_id, $blog_id);
update_option(self::SETTING_LAST_ROLE_SYNC, time());
}
}
private function _on_role_change() {
update_site_option(self::SETTING_LAST_ROLE_CHANGE, time());
}
/**
@@ -121,9 +162,20 @@ class Controller_Permissions {
return $wpdb->get_col("SELECT blogs.blog_id FROM {$wpdb->site} sites JOIN {$wpdb->blogs} blogs ON blogs.site_id=sites.id AND blogs.path=sites.path");
}
}
private function get_sites($from, $count) {
/**
* Returns an array of all multisite `blog_id` values, optionally limiting the result to the subset between
* ($from, $from + $count].
*
* @param int $from
* @param int $count
* @return array
*/
private function get_sites($from = 0, $count = 0) {
global $wpdb;
if ($from === 0 && $count === 0) {
return $wpdb->get_col("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0 ORDER BY blog_id ");
}
return $wpdb->get_col($wpdb->prepare("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0 AND blog_id > %d ORDER BY blog_id LIMIT %d", $from, $count));
}
@@ -162,7 +214,7 @@ class Controller_Permissions {
}
public function allow_2fa_self($role_name) {
$this->on_role_change();
$this->_on_role_change();
if (is_multisite()) {
return $this->_add_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
}
@@ -172,7 +224,7 @@ class Controller_Permissions {
}
public function disallow_2fa_self($role_name) {
$this->on_role_change();
$this->_on_role_change();
if (is_multisite()) {
return $this->_remove_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
}
@@ -272,22 +324,125 @@ class Controller_Permissions {
$wp_roles->remove_cap($role_name, $cap);
return true;
}
/**
* Loads the role capability info for the multisite blog IDs in `$includedSites` and appends it to
* `$this->multisite_roles`. Role capability data that is already loaded will be skipped.
*
* @param array $includeSites An array of multisite blog IDs to load.
*/
private function _load_multisite_roles($includeSites) {
global $wpdb;
$needed = array_diff($includeSites, array_keys($this->multisite_roles));
if (empty($needed)) {
return;
}
$suffix = "user_roles";
$queries = array();
foreach ($needed as $b) {
$tables = $wpdb->tables('blog', true, $b);
$queries[] = "SELECT CAST(option_name AS CHAR UNICODE) AS option_name, CAST(option_value AS CHAR UNICODE) AS option_value FROM {$tables['options']} WHERE option_name LIKE '%{$suffix}'";
}
$chunks = array_chunk($queries, 50);
$options = array();
foreach ($chunks as $c) {
$rows = $wpdb->get_results(implode(' UNION ', $c), OBJECT_K);
foreach ($rows as $row) {
$options[$row->option_name] = $row->option_value;
}
}
$extractor = new Utility_MultisiteConfigurationExtractor($wpdb->base_prefix, $suffix);
foreach ($extractor->extract($options) as $site => $option) {
$this->multisite_roles[$site] = maybe_unserialize($option);
}
}
/**
* Returns an array of multisite roles. This is guaranteed to include the multisite blogs in `$includeSites` but may
* include others from earlier calls that are cached.
*
* @param array $includeSites An array for multisite blog IDs.
* @return array
*/
public function get_multisite_roles($includeSites) {
if ($this->multisite_roles === null) {
$this->multisite_roles = array();
}
$this->_load_multisite_roles($includeSites);
return $this->multisite_roles;
}
/**
* Returns the sites + roles that a user has on multisite. The structure of the returned array has the keys as the
* individual site IDs and the associated value as an array of the user's capabilities on that site.
*
* @param WP_User $user
* @return array
*/
public function get_multisite_roles_for_user($user) {
global $wpdb;
$roles = array();
$meta = get_user_meta($user->ID);
if (is_array($meta)) {
$extractor = new Utility_MultisiteConfigurationExtractor($wpdb->base_prefix, 'capabilities');
foreach ($extractor->extract($meta) as $site => $capabilities) {
if (!is_array($capabilities)) { continue; }
$capabilities = array_map('maybe_unserialize', $capabilities);
$localRoles = array();
foreach ($capabilities as $entry) {
foreach ($entry as $role => $state) {
if ($state)
$localRoles[$role] = true;
}
}
$roles[$site] = array_keys($localRoles);
}
}
return $roles;
}
public function get_all_roles($user) {
global $wpdb;
if (is_multisite()) {
$roles = array();
if (is_super_admin($user->ID))
$roles[] = 'super-admin';
foreach (get_blogs_of_user($user->ID) as $id => $blog) {
switch_to_blog($id);
$blogUser = new \WP_User($user->ID);
$roles = array_merge($roles, $blogUser->roles);
restore_current_blog();
if (is_super_admin($user->ID)) {
$roles['super-admin'] = true;
}
return array_unique($roles);
foreach ($this->get_multisite_roles_for_user($user) as $site => $siteRoles) {
foreach ($siteRoles as $role) {
$roles[$role] = true;
}
}
return array_keys($roles);
}
else {
return $user->roles;
}
}
public function does_user_have_multisite_capability($user, $capability) {
$userRoles = $this->get_multisite_roles_for_user($user);
if (in_array('super-admin', $userRoles)) {
return true;
}
$blogRoles = $this->get_multisite_roles(array_keys($userRoles));
$blogs = get_blogs_of_user($user->ID);
foreach ($blogs as $blogId => $blog) {
$blogId = (int) $blogId;
if (!array_key_exists($blogId, $userRoles) || !array_key_exists($blogId, $blogRoles)) { continue; } //Blog with ID `$blogId` should be ignored
foreach ($userRoles[$blogId] as $userRole) {
if (!array_key_exists($userRole, $blogRoles[$blogId]) || !array_key_exists('capabilities', $blogRoles[$blogId][$userRole])) { continue; } //Sanity check for needed keys, should not happen
$capabilities = $blogRoles[$blogId][$userRole]['capabilities'];
if (array_key_exists($capability, $capabilities) && $capabilities[$capability]) { return true; }
}
}
return false;
}
}

View File

@@ -188,42 +188,11 @@ class Controller_Users {
*/
public function can_activate_2fa($user) {
if (is_multisite() && !is_super_admin($user->ID)) {
$blogs = get_blogs_of_user($user->ID);
foreach ($blogs as $id => $info) {
if ($this->_user_can_for_blog($user, $id, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
return true;
}
}
return false;
return Controller_Permissions::shared()->does_user_have_multisite_capability($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF);
}
return user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF);
}
/**
* Implementation of current_user_can_for_blog that works for an arbitrary user.
*
* @param int $user_id
* @param int $blog_id
* @param string $capability
* @return bool
*/
private function _user_can_for_blog($user_id, $blog_id, $capability) {
$switched = is_multisite() ? switch_to_blog($blog_id) : false;
$user = new \WP_User($user_id);
$args = array_slice(func_get_args(), 2);
$args = array_merge(array($capability), $args);
$can = call_user_func_array(array($user, 'has_cap'), $args);
if ($switched) {
restore_current_blog();
}
return $can;
}
/**
* Returns whether or not any user has 2FA activated.
*

View File

@@ -82,6 +82,8 @@ class Controller_WordfenceLS {
add_action('init', array($this, '_wordpress_init'));
if ($this->is_shortcode_enabled())
add_action('wp_enqueue_scripts', array($this, '_handle_shortcode_prerequisites'));
Controller_Permissions::_init_actions();
}
public function _wordpress_init() {
@@ -214,6 +216,7 @@ END
public function _uninstall_plugin() {
Controller_Time::shared()->uninstall();
Controller_Permissions::shared()->uninstall();
foreach (array(self::VERSION_KEY) as $opt) {
if (is_multisite() && function_exists('delete_network_option')) {
@@ -348,7 +351,7 @@ END
->enqueue();
wp_enqueue_style('wordfence-ls-login', Model_Asset::css('login.css'), array(), WORDFENCE_LS_VERSION);
wp_localize_script('wordfence-ls-login', 'WFLSVars', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'ajaxurl' => Utility_URL::relative_admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp-ajax'),
'recaptchasitekey' => Controller_Settings::shared()->get(Controller_Settings::OPTION_RECAPTCHA_SITE_KEY),
'useCAPTCHA' => $useCAPTCHA,
@@ -361,7 +364,7 @@ END
private function get_2fa_management_script_data() {
return array(
'WFLSVars' => array(
'ajaxurl' => admin_url('admin-ajax.php'),
'ajaxurl' => Utility_URL::relative_admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp-ajax'),
'modalTemplate' => Model_View::create('common/modal-prompt', array('title' => '${title}', 'message' => '${message}', 'primaryButton' => array('id' => 'wfls-generic-modal-close', 'label' => __('Close', 'wordfence'), 'link' => '#')))->render(),
'modalNoButtonsTemplate' => Model_View::create('common/modal-prompt', array('title' => '${title}', 'message' => '${message}'))->render(),

View File

@@ -0,0 +1,19 @@
<?php
namespace WordfenceLS;
class Utility_MeasuredString {
public $string;
public $length;
public function __construct($string) {
$this->string = $string;
$this->length = strlen($string);
}
public function __toString() {
return $this->string;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace WordfenceLS;
class Utility_Multisite {
/**
* Returns an array of all active multisite blogs (if `$blogIds` is `null`) or a list of active multisite blogs
* filtered to only those in `$blogIds` if non-null.
*
* @param array|null $blogIds
* @return array
*/
public static function retrieve_active_sites($blogIds = null) {
$args = array(
'number' => '', /* WordPress core passes an empty string which appears to remove the result set limit */
'update_site_meta_cache' => false, /* Defaults to true which is not desirable for this use case */
//Ignore archived/spam/deleted sites
'archived' => 0,
'spam' => 0,
'deleted' => 0
);
if ($blogIds !== null) {
$args['site__in'] = $blogIds;
}
if (function_exists('get_sites')) {
return get_sites($args);
}
global $wpdb;
if ($blogIds !== null) {
$blogIdsQuery = implode(',', wp_parse_id_list($args['site__in']));
return $wpdb->get_results("SELECT * FROM {$wpdb->blogs} WHERE blog_id IN ({$blogIdsQuery}) AND archived = 0 AND spam = 0 AND deleted = 0");
}
return $wpdb->get_results("SELECT * FROM {$wpdb->blogs} WHERE archived = 0 AND spam = 0 AND deleted = 0");
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace WordfenceLS;
class Utility_MultisiteConfigurationExtractor {
private $prefix, $suffix;
private $suffixOffset;
public function __construct($prefix, $suffix) {
$this->prefix = new Utility_MeasuredString($prefix);
$this->suffix = new Utility_MeasuredString($suffix);
$this->suffixOffset = -$this->suffix->length;
}
/**
* Parses a `get_user_meta` result array into a more usable format. The input array will be something similar to
* [
* 'wp_capabilities' => '...',
* 'wp_3_capabilities' => '...',
* 'wp_4_capabilities' => '...',
* 'wp_10_capabilities' => '...',
* ]
*
* This will return
* [
* 1 => '...',
* 3 => '...',
* 4 => '...',
* 10 => '...',
* ]
*
* @param array $values
* @return array
*/
private function parseBlogIds($values) {
$parsed = array();
foreach ($values as $key => $value) {
if (substr($key, $this->suffixOffset) === $this->suffix->string && strpos($key, (string) $this->prefix) === 0) {
$blogId = substr($key, $this->prefix->length, strlen($key) - $this->prefix->length + $this->suffixOffset);
if (empty($blogId)) {
$parsed[1] = $value;
}
else if (substr($blogId, -1) === '_') {
$parsed[(int) $blogId] = $value;
}
}
}
return $parsed;
}
/**
* Filters $values, which is the resulting array from `$this->parseBlogIds` so it contains only the values for the
* sites in $sites.
*
* @param array $values
* @param array $sites
* @return array
*/
private function filterValues($values, $sites) {
$filtered = array();
foreach ($sites as $site) {
$blogId = (int) $site->blog_id;
$filtered[$blogId] = $values[$blogId];
}
return $filtered;
}
/**
* Processes a `get_user_meta` result array to re-key it so the keys are the numerical ID of all multisite blog IDs
* in `$values` that are still in an active state.
*
* @param array $values
* @return array
*/
public function extract($values) {
$parsed = $this->parseBlogIds($values);
if (empty($parsed))
return $parsed;
$sites = Utility_Multisite::retrieve_active_sites(array_keys($parsed));
return $this->filterValues($parsed, $sites);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace WordfenceLS;
class Utility_URL {
/**
* Similar to WordPress' `admin_url`, this returns a host-relative URL for the given path. It may be used to avoid
* canonicalization issues with CORS (e.g., the site is configured for the www. variant of the URL but doesn't forward
* the other).
*
* @param string $path
* @return string
*/
public static function relative_admin_url($path = '') {
$url = admin_url($path);
$components = parse_url($url);
$s = $components['path'];
if (!empty($components['query'])) {
$s .= '?' . $components['query'];
}
if (!empty($components['fragment'])) {
$s .= '#' . $components['fragment'];
}
return $s;
}
}