Uname: Linux webm005.cluster107.gra.hosting.ovh.net 5.15.167-ovh-vps-grsec-zfs-classid #1 SMP Tue Sep 17 08:14:20 UTC 2024 x86_64
User: 6036 (villadal)
Group: 100 (users)
Disabled functions: NONE
Safe mode: On[ PHPinfo ]
//home/villadal/www/wp-content/plugins/wordfence///lib      ( Reset | Go to )
File Name: wfAuditLog.php
Edit
<?php

require_once(__DIR__ '/audit-log/wfAuditLogObserversWordPressCoreUser.php');
require_once(
__DIR__ '/audit-log/wfAuditLogObserversWordPressCoreSite.php');
require_once(
__DIR__ '/audit-log/wfAuditLogObserversWordPressCoreMultisite.php');
require_once(
__DIR__ '/audit-log/wfAuditLogObserversWordPressCoreContent.php');
require_once(
__DIR__ '/audit-log/wfAuditLogObserversWordfence.php');
require_once(
__DIR__ '/audit-log/wfAuditLogObserversPreview.php');

/**
 * Class wfAuditLog
 * 
 * Hooks into a variety of actions/filters to collect relevant data that can be recorded in an audit log. The data 
 * collected is focused around attack surfaces such as user registration and content insertion, but all attempts are
 * made to exclude potentially sensitive values from being recorded (e.g., for user profile changes, only the field
 * names are recorded).
 * 
 * Data is recorded into an intermediate table on the site itself, and a send action is scheduled. When this action
 * triggers, a send payload up to the maximum transmit count is generated. The payload is then automatically expanded so
 * that no partial request is sent, only full requests. Once sent, these are removed from the intermediate table, and
 * we check to see if there are more remaining to be sent, scheduling another send if so.
 * 
 * Because of how some of the hooks are called, there are three different points at which data may be recorded:
 * 
 * 1. At the moment the hook is called. This is most common and used for one-off actions where the recording should be 
 *    performed at that time.
 * 2. Pre-filters/actions. For these, an earlier hook in the flow is listened for, and we record state data for later
 *    use by the desired hook. This is typically used for deletions where we want some value from the record before it
 *    gets deleted.
 * 3. At the end of the request. For actions that may reasonably called multiple times in the same request (e.g., adding
 *    multiple capabilities to a role), we only need to record a single record of that action so this is done via a 
 *    coalescer at the end just prior to the request ending.
 * 
 * Some hooks do record for multiple events due to how overloaded some data structures are in WP. For example, many 
 * types are ultimately stored in `wp_posts` despite not being posts so the hooks surrounding that must check for the
 * context to determine which event to actually record.
 */
class wfAuditLog {
    const 
AUDIT_LOG_MODE_DEFAULT 'default'//Resolves to one of the below based on license type
    
const AUDIT_LOG_MODE_DISABLED 'disabled';
    const 
AUDIT_LOG_MODE_PREVIEW 'preview';
    const 
AUDIT_LOG_MODE_SIGNIFICANT 'significant';
    const 
AUDIT_LOG_MODE_ALL 'all';
    
    
//These category constants are used to divide events into the groupings in the event listing, one per event even if the event could fit under multiple
    
const AUDIT_LOG_CATEGORY_AUTHENTICATION 'authentication';
    const 
AUDIT_LOG_CATEGORY_USER_PERMISSIONS 'user-permissions';
    const 
AUDIT_LOG_CATEGORY_PLUGINS_THEMES_UPDATES 'plugins-themes-updates';
    const 
AUDIT_LOG_CATEGORY_SITE_SETTINGS 'site-settings';
    const 
AUDIT_LOG_CATEGORY_MULTISITE 'multisite';
    const 
AUDIT_LOG_CATEGORY_CONTENT 'content';
    const 
AUDIT_LOG_CATEGORY_FIREWALL 'firewall';
    
    const 
AUDIT_LOG_MAX_SAMPLES 20//Max number of requests to store in the local summary, each of which may have one or more events
    
    
const AUDIT_LOG_HEARTBEAT 'heartbeat'//A unique event that is sent to signal the audit log is functioning even if no other events have triggered, not displayed on the front end
    
    
private $_pending = array();
    private 
$_coalescers = array();
    private 
$_destructRegistered false;
    
    private 
$_state = array();
    private 
$_performingFinalization false;
    
    protected static 
$initialCoreVersion;
    protected static 
$initialMode;
    
    public static function 
shared() {
        static 
$_shared null;
        if (
$_shared === null) {
            
$_shared = new wfAuditLog();
        }
        return 
$_shared;
    }
    
    
/**
     * Returns the events that will cause an immediate send rather than waiting for the cron event to execute. 
     * Individual observer grouping subclasses must override this and return their subset of the event categories. The 
     * primary audit log class will return an array of all observer groupings merged together.
     * 
     * @return array
     */
    
public static function immediateSendEvents() {
        static 
$eventCache null;
        if (
$eventCache === null) {
            
$eventCache = array();
            
            
$observers self::_observers();
            foreach (
$observers as $o) {
                
$merging call_user_func(array($o'immediateSendEvents'));
                
$eventCache array_merge($eventCache$merging);
            }
        }
        
        return 
$eventCache;
    }
    
    
/**
     * Returns the event categories for use in the Audit Log page's UI. Individual observer grouping subclasses
     * must override this and return their subset of the event categories. The primary audit log class will return an 
     * array of all observer groupings merged together.
     *
     *
     * @return array
     */
    
public static function eventCategories() {
        static 
$categoryCache null;
        if (
$categoryCache === null) {
            
$categoryCache = array();
            
            
$observers self::_observers();
            foreach (
$observers as $o) {
                
$merging call_user_func(array($o'eventCategories'));
                foreach (
$merging as $category => $events) {
                    if (isset(
$categoryCache[$category])) {
                        
$categoryCache[$category] = array_merge($categoryCache[$category], $events);
                    }
                    else {
                        
$categoryCache[$category] = $events;
                    }
                }
            }
        }
        
        return 
$categoryCache;
    }
    
    
/**
     * Returns the category for $event, null if not found.
     * 
     * @param string $event
     * @return string|null
     */
    
public static function eventCategory($event) {
        static 
$reverseCategoryMapCache null;
        if (
$reverseCategoryMapCache === null) {
            
$reverseCategoryMapCache = array();
            
$categories self::eventCategories();
            foreach (
$categories as $category => $events) {
                
$reverseCategoryMapCache array_merge($reverseCategoryMapCachearray_fill_keys($events$category));
            }
        }
        
        if (isset(
$reverseCategoryMapCache[$event])) {
            return 
$reverseCategoryMapCache[$event];
        }
        return 
null;
    }
    
    
/**
     * Returns the event names suitable for display in the Audit Log page's UI. Individual observer grouping subclasses 
     * must override this and return their subset of the event names. The primary audit log class will return an array 
     * of all observer groupings merged together.
     * 
     * 
     * @return array
     */
    
public static function eventNames() {
        static 
$nameCache null;
        if (
$nameCache === null) {
            
$nameCache = array();
            
            
$observers self::_observers();
            foreach (
$observers as $o) {
                
$nameCache array_merge($nameCachecall_user_func(array($o'eventNames')));
            }
        }
        
        return 
$nameCache;
    }
    
    
/**
     * Returns the display name for the given event identifier.
     * 
     * @param string $event
     * @return string
     */
    
public static function eventName($event) {
        
$map self::eventNames();
        if (isset(
$map[$event])) {
            return 
$map[$event];
        }
        return 
__('Unknown Events''wordfence');
    }
    
    
/**
     * Returns the event rate limiters for use in preprocessing events that occur. A rate limiter for an event type 
     * should use the passed $auditLog and $payload values to determine whether the proposed event should be recorded. 
     * The primary audit log class will return an array of all observer groupings merged together.
     *
     *
     * @return array
     */
    
public static function eventRateLimiters() {
        static 
$rateLimiterCache null;
        if (
$rateLimiterCache === null) {
            
$rateLimiterCache = array();
            
            
$observers self::_observers();
            foreach (
$observers as $o) {
                
$rateLimiterCache array_merge($rateLimiterCachecall_user_func(array($o'eventRateLimiters')));
            }
        }
        
        return 
$rateLimiterCache;
    }
    
    
/**
     * Consumes the rate limiter by setting a transient for the given $ttl. Currently this just allows a bucket of one,
     * but this could be refactored in the future to allow variable rate limits.
     * 
     * @param string $event
     * @param string $payloadSignature
     * @param int $ttl Default is 10 minutes
     */
    
protected static function _rateLimiterConsume($event$payloadSignature$ttl 600) {
        
$key 'wordfenceAuditEvent:' $event ':' $payloadSignature;
        
set_transient($keytime(), $ttl);
    }
    
    
/**
     * Returns whether or not the rate limiter is available. The return value is `true` if it is, otherwise `false`.
     * 
     * @param string $event
     * @param string $payloadSignature
     * @return bool
     */
    
protected static function _rateLimiterCheck($event$payloadSignature) {
        
$key 'wordfenceAuditEvent:' $event ':' $payloadSignature;
        return !
get_transient($key);
    }
    
    
/**
     * Recursively computes a hash for the given payload in a deterministic way. This may be used in rate limiter
     * implementations for deduplication checks.
     * 
     * @param mixed $payload
     * @param null|HashContext $hasher
     * @return bool|string
     */
    
protected static function _normalizedPayloadHash($payload$hasher null) {
        
$first is_null($hasher);
        if (
$first) {
            
$hasher hash_init('sha256');
        }
        
        if (
is_array($payload) || is_object($payload)) {
            
$payload = (array) $payload;
            
$keys array_keys($payload);
            
sort($keysSORT_REGULAR);
            foreach (
$keys as $k) {
                
$v $payload[$k];
                
hash_update($hasher$k);
                
self::_normalizedPayloadHash($v$hasher);
            }
        }
        else if (
is_scalar($payload)) {
            
hash_update($hasher$payload);
        }
        
        if (
$first) {
            return 
hash_final($hasher);
        }
        return 
true;
    }
    
    
/**
     * Returns an array of all observer groupings.
     * 
     * @return array
     */
    
private static function _observers() {
        return array(
            
wfAuditLogObserversWordPressCoreUser::class,
            
wfAuditLogObserversWordPressCoreSite::class,
            
wfAuditLogObserversWordPressCoreMultisite::class,
            
wfAuditLogObserversWordPressCoreContent::class,
            
wfAuditLogObserversWordfence::class,
        );
    }
    
    
/**
     * Registers the observers for this class's chunk of functionality that should run regardless of other settings.
     * These observers are expected to do their own check and application of settings like the audit log's mode or
     * the `Participate in the Wordfence Security Network` setting.
     *
     * @param wfAuditLog $auditLog
     */
    
protected static function _registerForcedObservers($auditLog) {
        
//Individual forced observer groupings may override this
    
}
    
    
/**
     * Registers the observers for this class's chunk of functionality.
     *
     * @param wfAuditLog $auditLog
     */
    
protected static function _registerObservers($auditLog) {
        
//Individual observer groupings will override this
    
}
    
    
/**
     * Registers the data gatherers for this class's chunk of functionality. These are secondary hooks to support 
     * intermediate data gathering (e.g., grabbing the user attempting to authenticate even if it fails)
     *
     * @param wfAuditLog $auditLog
     */
    
protected static function _registerDataGatherers($auditLog) {
        
//Individual data gatherer groupings will override this
    
}
    
    
/**
     * Registers the coalescers for this class's chunk of functionality.
     *
     * @param wfAuditLog $auditLog
     */
    
protected static function _registerCoalescers($auditLog) {
        
//Individual coalescer groupings will override this
    
}
    
    public static function 
heartbeat() {
        if (
wfAuditLog::shared()->mode() != wfAuditLog::AUDIT_LOG_MODE_DISABLED && wfAuditLog::shared()->mode() != wfAuditLog::AUDIT_LOG_MODE_PREVIEW) {
            
wfAuditLog::shared()->_recordAction(self::AUDIT_LOG_HEARTBEAT);
        }
    }
    
    
/**
     * Returns the effective audit log mode after factoring in the active license type and resolving the default based 
     * on that type. Will be one of the wfAuditLog::AUDIT_LOG_MODE_* constants that is not AUDIT_LOG_MODE_DEFAULT.
     * 
     * @return string
     */
    
public function mode() {
        require(
__DIR__ '/wfVersionSupport.php'); /** @var $wfFeatureWPVersionAuditLog */
        
require(ABSPATH WPINC '/version.php'); /** @var string $wp_version */
        
if (version_compare($wp_version$wfFeatureWPVersionAuditLog'<')) {
            return 
self::AUDIT_LOG_MODE_DISABLED;
        }
        
        
$mode wfConfig::get('auditLogMode'self::AUDIT_LOG_MODE_DEFAULT);
        
$license wfLicense::current();
        if (!
$license->isPaidAndCurrent() || !$license->isAtLeastPremium()) {
            if (
$mode == self::AUDIT_LOG_MODE_DISABLED) {
                return 
$mode;
            }
            return 
self::AUDIT_LOG_MODE_PREVIEW;
        }
        
        if (
$mode == self::AUDIT_LOG_MODE_DEFAULT) {
            if (!
$license->isAtLeastCare()) {
                return 
self::AUDIT_LOG_MODE_PREVIEW;
            }
            
            return 
self::AUDIT_LOG_MODE_SIGNIFICANT;
        }
        
        return 
$mode;
    }
    
    public function 
registerHooks() {
        
self::$initialMode $this->mode();
        
        require(
ABSPATH WPINC '/version.php'); /** @var string $wp_version */
        
self::$initialCoreVersion $wp_version;
        
        
$observers self::_observers();
        foreach (
$observers as $o) {
            
call_user_func(array($o'_registerForcedObservers'), $this);
        }
        
        if (
$this->mode() == self::AUDIT_LOG_MODE_DISABLED) {
            return;
        }
        
        if (
$this->mode() == self::AUDIT_LOG_MODE_PREVIEW) { //When in preview mode, we register the local-only observers to keep the preview data fresh locally
            
wfAuditLogObserversPreview::_registerObservers($this);
            
wfAuditLogObserversPreview::_registerDataGatherers($this);
            
wfAuditLogObserversPreview::_registerCoalescers($this);
            return;
        }
        
        foreach (
$observers as $o) {
            
call_user_func(array($o'_registerObservers'), $this);
            
call_user_func(array($o'_registerDataGatherers'), $this);
            
call_user_func(array($o'_registerCoalescers'), $this);
        }
    }
    
    
/**
     * Convenience method to add a listener for one or more WordPress hooks. This simplifies the normal flow of adding
     * a listener by using introspection on the passed callable to pass the correct arguments.
     * 
     * @param array|string $hooks
     * @param callable $closure
     * @param string $type
     */
    
protected function _addObserver($hooks$closure$type 'action') {
        if (!
is_array($hooks)) {
            
$hooks = array($hooks);
        }
        
        try {
            
$introspection = new ReflectionFunction($closure);
            if (
$type == 'action') {
                foreach (
$hooks as $hook) {
                    
add_action($hook$closure1$introspection->getNumberOfParameters());
                }
            }
            else if (
$type == 'filter') {
                foreach (
$hooks as $hook) {
                    
add_filter($hook$closure1$introspection->getNumberOfParameters());
                }
            }
        }
        catch (
Exception $e) {
            
//Ignore
        
}
    }
    
    protected function 
_addCoalescer($closure) {
        
$this->_coalescers[] = $closure;
    }
    
    
/**
     * Returns whether or not a state value exists for the given key/blog pair.
     * 
     * @param string $key
     * @param int $id An ID when tracking multiple potential states. May be the blog ID if multisite or a user ID.
     * @return bool
     */
    
protected function _hasState($key$id 1) {
        if (
$id 0) {
            
$id 0;
        }
        
        if (!isset(
$this->_state[$id])) {
            return 
false;
        }
        
        return isset(
$this->_state[$id][$key]);
    }
    
    
/**
     * Stores a state value under the key/blog pair for later use in this request.
     * 
     * @param string $key
     * @param mixed $value
     * @param int $id An ID when tracking multiple potential states. May be the blog ID if multisite or a user ID.
     */
    
protected function _trackState($key$value$id 1) {
        if (
$id 0) {
            
$id 0;
        }
        
        if (!isset(
$this->_state[$id])) {
            
$this->_state[$id] = array();
        }
        
        
$this->_state[$id][$key] = $value;
    }
    
    
/**
     * Returns the state value for the key/blog pair if present, otherwise null.
     * 
     * @param string $key
     * @param int $id An ID when tracking multiple potential states. May be the blog ID if multisite or a user ID.
     * @return mixed|null
     */
    
protected function _getState($key$id 1) {
        if (
$id 0) {
            
$id 0;
        }
        
        if (!isset(
$this->_state[$id]) || !isset($this->_state[$id][$key])) {
            return 
null;
        }
        
        return 
$this->_state[$id][$key];
    }
    
    
/**
     * Returns all site(s)' state values for $key if present. They keys in the returned array are the blog ID.
     * 
     * @param string $key
     * @return array Will have at most 1 entry for single-site, potentially many for multisite when applicable.
     */
    
protected function _getAllStates($key) {
        
$result = array();
        foreach (
$this->_state as $id => $state) {
            if (isset(
$state[$key])) {
                
$result[$id] = $state[$key];
            }
        }
        return 
$result;
    }
    
    
/**
     * Record the action and metadata for later sending to the audit log.
     * 
     * @param string $action
     * @param array $metadata
     * @param bool $appendToExisting When true, does not create a new entry and instead only appends to entries of the same $action
     */
    
protected function _recordAction($action$metadata = array(), $appendToExisting false) {
        
$rateLimiters self::eventRateLimiters();
        if (isset(
$rateLimiters[$action])) {
            if (!
$rateLimiters[$action]($this$metadata)) {
                return;
            }
        }
        
        if (
$appendToExisting) {
            foreach (
$this->_pending as &$entry) {
                if (
$entry['action'] == $action) {
                    
$entry['metadata'] = array_merge($entry['metadata'], $metadata);
                }
            }
            return;
        }
        
        
$path null;
        
$body null;
        if (@
php_sapi_name() === 'cli' || !array_key_exists('REQUEST_METHOD'$_SERVER)) {
            if (isset(
$_SERVER['argv']) && is_array($_SERVER['argv']) && count($_SERVER['argv']) > 0) {
                
$path $_SERVER['argv'][0] . ' ' implode(' 'array_map(function($p) { return '\'' addcslashes($p'\'') . '\''; }, array_slice($_SERVER['argv'], 1)));
                
$body = array('type' => 'cli''files' => array(), 'parameters' => array('argv' => $_SERVER['argv']));
            }
            
$method 'CLI';
        }
        else {
            
$path $_SERVER['REQUEST_URI'];
            
$method $_SERVER['REQUEST_METHOD'];
            if (
$_SERVER['REQUEST_METHOD'] != 'GET') {
                
$body $this->_sanitizeRequestBody();
            }
        }
        
        
$user wp_get_current_user();
        
$entry = array(
            
'action' => $action,
            
'time' => wfUtils::normalizedTime(),
            
'metadata' => $metadata,
            
'context' => array(
                
'ip' => wfUtils::getIP(),
                
'path' => $path,
                
'method' => $method,
                
'body' => $body,
                
'user_id' => $user $user->ID 0,
                
'userdata' => $this->_sanitizeUserdata($user),
            ),
        );
        
        if (
is_multisite()) {
            
$network get_network();
            
$blog get_blog_details();
            
$entry['multisite'] = $this->_sanitizeMultisiteData($network$blog);
        }
        
        
$this->_pending[] = $entry;
        
        
$this->_needsDestruct();
    }
    
    
/**
     * Finalizes the pending actions. If cron is disabled or one of the types is on the immedate send list, they are 
     * finalized by immediately sending to the audit log. Otherwise, they are saved to the intermediate storage table 
     * and a send is scheduled.
     */
    
private function _savePending() {
        if (!empty(
$this->_pending)) {
            
$sendImmediately false;
            
$immediateSend self::immediateSendEvents();
            
$payload = array();
            foreach (
$this->_pending as $data) {
                
$time $data['time'];
                unset(
$data['time']);
                
                if (
$data['action'] == self::AUDIT_LOG_HEARTBEAT) { //Minimize payload for heartbeat
                    
$payload[] = array(
                        
'type' => $data['action'],
                        
'data' => array(),
                        
'event_time' => $time,
                    );
                }
                else {
                    
$payload[] = array(
                        
'type' => $data['action'],
                        
'data' => $data,
                        
'event_time' => $time,
                    );
                }
                
                
$sendImmediately = ($sendImmediately || in_array($data['action'], $immediateSend));
            }
            
            if (
defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
                
$sendImmediately true;
            }
            
            if (
$sendImmediately && !wfCentral::isConnected()) {
                
$this->_saveEventsToTable($payload);
                if (
$ts wp_next_scheduled('wordfence_batchSendAuditEvents')) {
                    
$this->_unscheduleSendPendingAuditEvents($ts);
                }
                
$this->_scheduleSendPendingAuditEvents();
                
$this->_pending = array();
                return;
            }
            
            
$before $payload;
            if (
$sendImmediately) {
                
$requestID wfConfig::atomicInc('auditLogRequestNumber');
                
                foreach (
$payload as &$p) {
                    
$p['data'] = json_encode($p['data']);
                    
$p['request_id'] = $requestID;
                }
            }
            
            try {
                if (
$this->_sendAuditLogEvents($payload$sendImmediately)) {
                    
$this->_pending = array();
                }
            }
            catch (
wfAuditLogSendFailedException $e) {
                if (
$sendImmediately) {
                    
$this->_saveEventsToTable($before);
                    if (
$ts wp_next_scheduled('wordfence_batchSendAuditEvents')) {
                        
$this->_unscheduleSendPendingAuditEvents($ts);
                    }
                    
$this->_scheduleSendPendingAuditEvents(true);
                    
$this->_pending = array();
                }
            }
        }
    }
    
    protected function 
_needsDestruct() {
        if (!
$this->_destructRegistered) {
            
register_shutdown_function(array($this'_lastAction'));
            
$this->_destructRegistered true;
        }
    }
    
    
/**
     * Performed as a shutdown handler to finalize all pending actions.
     * 
     * Note: must remain `public` for PHP 7 compatibility
     */
    
public function _lastAction() {
        global 
$wpdb;
        
$suppressed $wpdb->suppress_errors(!(defined('WFWAF_DEBUG') && WFWAF_DEBUG));
        
        
$this->_performingFinalization true;
        foreach (
$this->_coalescers as $c) {
            
call_user_func($c);
        }
        
$this->_coalescers = array();
        
$this->_savePending();
        
$this->_performingFinalization false;
        
        
$wpdb->suppress_errors($suppressed);
    }
    
    public function 
isFinalizing() {
        return 
$this->_performingFinalization;
    }
    
    
/**
     * Performs the actual send of $events to the audit log if $sendImmediately is truthy, otherwise it writes them to
     * the intermediate storage table and schedules a send.
     * 
     * @param array $events
     * @param bool $sendImmediately
     * @return bool
     * @throws wfAuditLogSendFailedException
     */
    
private function _sendAuditLogEvents($events$sendImmediately false) {
        if (empty(
$events)) {
            return 
true;
        }
        
        if (!
wfCentral::isConnected()) {
            return 
false//This will cause it to mark them as unsent and try again later
        
}
        
        if (
$sendImmediately) {
            
$payload = array();
            foreach (
$events as $e) {
                
$payload[] = self::_formatEventForTransmission($e);
            }
            
            
$siteID wfConfig::get('wordfenceCentralSiteID');
            
$request = new wfCentralAuthenticatedAPIRequest('/site/' $siteID '/audit-log''POST', array(
                
'data' => $payload,
            ));
            try {
                
$doing_cron function_exists('wp_doing_cron'/* WP >= 4.8 */ wp_doing_cron() : (defined('DOING_CRON') && DOING_CRON);
                
$response $request->execute($doing_cron 10 3);
                
                if (
$response->isError()) {
                    throw new 
wfAuditLogSendFailedException();
                }
                
                
//Group by request and update the local preview
                
$preview = array();
                foreach (
$payload as $r) {
                    if (!isset(
$preview[$r['attributes']['request_id']])) {
                        
$preview[$r['attributes']['request_id']] = array();
                    }
                    
$preview[$r['attributes']['request_id']][] = array($r['attributes']['type'], $r['attributes']['event_time']);
                }
                
uksort($preview, function($k1$k2) {
                    if (
$k1 == $k2) { return 0; }
                    return (
$k1 $k2) ? : -1;
                });
                
$this->_updateAuditPreview(array_values($preview));
            }
            catch (
Exception $e) {
                if (!
defined('WORDFENCE_DEACTIVATING') || !WORDFENCE_DEACTIVATING) { wfCentralAPIRequest::handleInternalCentralAPIError($e); }
                throw new 
wfAuditLogSendFailedException();
            }
            catch (
Throwable $t) {
                if (!
defined('WORDFENCE_DEACTIVATING') || !WORDFENCE_DEACTIVATING) { wfCentralAPIRequest::handleInternalCentralAPIError($t); }
                throw new 
wfAuditLogSendFailedException();
            }
        }
        else {
            
$this->_saveEventsToTable($events$sendImmediately);
            
            if ((
$ts $this->_isScheduledAuditEventCronOverdue()) || $sendImmediately) {
                if (
$ts) {
                    
$this->_unscheduleSendPendingAuditEvents($ts);
                }
                
self::sendPendingAuditEvents();
            }
            else {
                
$this->_scheduleSendPendingAuditEvents();
            }
        }
        
        return 
true;
    }
    
    private function 
_saveEventsToTable($events, &$sendImmediately false) {
        
$requestID wfConfig::atomicInc('auditLogRequestNumber');
        
        
$wfdb = new wfDB();
        
$table_wfAuditEvents wfDB::networkTable('wfAuditEvents');
        
$query "INSERT INTO {$table_wfAuditEvents} (`type`, `data`, `event_time`, `request_id`, `state`, `state_timestamp`) VALUES ";
        
$query .= implode(', 'array_fill(0count($events), "('%s', '%s', %f, %d, 'new', NOW())"));
        
        
$immediateSendTypes self::immediateSendEvents();
        
$args = array();
        foreach (
$events as $e) {
            
$sendImmediately $sendImmediately || in_array($e['type'], $immediateSendTypes);
            
$args[] = $e['type'];
            
$args[] = json_encode($e['data']);
            
$args[] = $e['event_time'];
            
$args[] = $requestID;
        }
        
$wfdb->queryWriteArray($query$args);
    }
    
    
/**
     * Sends any pending audit events up to the limit (default 100). The list will automatically expand if needed to include 
     * only complete requests so that no partial requests are sent.
     * 
     * If the events fail to send or there are more remaining, another future send will be scheduled if $scheduleFollowup is truthy.
     * 
     * @param int $limit
     * @param bool $scheduleFollowup Whether or not to schedule a followup send if there are more events pending, if false also unschedules any pending cron
     */
    
public static function sendPendingAuditEvents($limit 100$scheduleFollowup true) {
        
$wfdb = new wfDB();
        
$table_wfAuditEvents wfDB::networkTable('wfAuditEvents');
        
        
$limit intval($limit);
        
$rawEvents $wfdb->querySelect("SELECT * FROM {$table_wfAuditEvents} WHERE `state` = 'new' ORDER BY `id` ASC LIMIT {$limit}");
        if (empty(
$rawEvents)) {
            return;
        }
        
        
//Grab the entirety of the last request ID, even if it's beyond the 100 item limit
        
$last wfUtils::array_last($rawEvents);
        
$extendedID = (int) $last['id'];
        
$extendedRequestID = (int) $last['request_id'];
        
$extendedEvents $wfdb->querySelect("SELECT * FROM {$table_wfAuditEvents} WHERE `state` = 'new' AND `id` > {$extendedID} AND `request_id` = {$extendedRequestID} ORDER BY `id` ASC");
        
$rawEvents array_merge($rawEvents$extendedEvents);
        
        
//Process for submission
        
$ids = array();
        foreach (
$rawEvents as $r) {
            
$ids[] = intval($r['id']);
        }
        
        
$idParam '(' implode(', '$ids) . ')';
        
$wfdb->queryWrite("UPDATE {$table_wfAuditEvents} SET `state` = 'sending', `state_timestamp` = NOW() WHERE `id` IN {$idParam}");
        try {
            if (
self::shared()->_sendAuditLogEvents($rawEventstrue)) {
                
$wfdb->queryWrite("UPDATE {$table_wfAuditEvents} SET `state` = 'sent', `state_timestamp` = NOW() WHERE `id` IN {$idParam}");
                
                if (
$scheduleFollowup) {
                    
self::checkForUnsentAuditEvents();
                }
            }
            else {
                
$wfdb->queryWrite("UPDATE {$table_wfAuditEvents} SET `state` = 'new', `state_timestamp` = NOW() WHERE `id` IN {$idParam}");
                if (
$scheduleFollowup) {
                    
self::shared()->_scheduleSendPendingAuditEvents();
                }
            }
            
            if (!
$scheduleFollowup) {
                if (
$ts wp_next_scheduled('wordfence_batchSendAuditEvents')) {
                    
self::shared()->_unscheduleSendPendingAuditEvents($ts);
                }
            }
        }
        catch (
wfAuditLogSendFailedException $e) {
            
$wfdb->queryWrite("UPDATE {$table_wfAuditEvents} SET `state` = 'new', `state_timestamp` = NOW() WHERE `id` IN {$idParam}");
            if (
$ts wp_next_scheduled('wordfence_batchSendAuditEvents')) {
                
self::shared()->_unscheduleSendPendingAuditEvents($ts);
            }
            
            if (!
defined('WORDFENCE_DEACTIVATING') || !WORDFENCE_DEACTIVATING) {
                
self::shared()->_scheduleSendPendingAuditEvents(true);
            }
        }
    }
    
    
/**
     * Formats the event record for transmission to Central for recording.
     * 
     * @param array $rawEvent
     * @return array
     */
    
private static function _formatEventForTransmission($rawEvent) {
        if (
$rawEvent['type'] == self::AUDIT_LOG_HEARTBEAT) { //Minimize payload for heartbeat
            
return array(
                
'type' => 'audit-event',
                
'attributes' => array(
                    
'type' => $rawEvent['type'],
                    
'event_time' => (int) $rawEvent['event_time'],
                    
'request_id' => (int) $rawEvent['request_id'],
                )
            );
        }
        
        
$data json_decode($rawEvent['data'], true);
        if (empty(
$data)) { $data = array(); }
        unset(
$data['action']);
        
$username null; if (!empty($data['context']['userdata']) && isset($data['context']['userdata']['user_login'])) { $username $data['context']['userdata']['user_login']; }
        
$ip null; if (!empty($data['context']['ip'])) { $ip $data['context']['ip']; unset($data['context']['ip']); }
        
$path null; if (!empty($data['context']['path'])) { $path $data['context']['path']; unset($data['context']['path']); }
        
$method null; if (!empty($data['context']['method'])) { $method $data['context']['method']; unset($data['context']['method']); }
        
$body null; if (!empty($data['context']['body'])) { $body $data['context']['body']; unset($data['context']['body']); }
        
        return array(
            
'type' => 'audit-event',
            
'attributes' => array(
                
'type' => $rawEvent['type'],
                
'username' => $username,
                
'ip_address' => $ip,
                
'method' => $method,
                
'path' => $path,
                
'request_body' => $body,
                
'data' => $data,
                
'event_time' => (int) $rawEvent['event_time'],
                
'request_id' => (int) $rawEvent['request_id'],
            )
        );
    }
    
    
/**
     * Schedules a cron for sending pending audit events.
     */
    
private function _scheduleSendPendingAuditEvents($forceDelay false) {
        if ((
self::$initialMode == self::AUDIT_LOG_MODE_DISABLED || self::$initialMode == self::AUDIT_LOG_MODE_PREVIEW) && ($this->mode() == self::AUDIT_LOG_MODE_DISABLED || $this->mode() == self::AUDIT_LOG_MODE_PREVIEW)) {
            return; 
//Do not schedule cron if mode is disabled/preview and was not recently put into that state
        
}
        
        
$delay 60;
        if (
$forceDelay || !wfCentral::isConnected()) {
            
$delay 3600;
        }
        
        if (!
defined('DONOTCACHEDB')) { define('DONOTCACHEDB'true); }
        
$notMainSite is_multisite() && !is_main_site();
        if (
$notMainSite) {
            global 
$current_site;
            
switch_to_blog($current_site->blog_id);
        }
        if (!
wp_next_scheduled('wordfence_batchSendAuditEvents')) {
            
wp_schedule_single_event(time() + $delay'wordfence_batchSendAuditEvents');
        }
        if (
$notMainSite) {
            
restore_current_blog();
        }
    }
    
    
/**
     * @param int $timestamp
     */
    
private function _unscheduleSendPendingAuditEvents($timestamp) {
        if (!
defined('DONOTCACHEDB')) { define('DONOTCACHEDB'true); }
        
$notMainSite is_multisite() && !is_main_site();
        if (
$notMainSite) {
            global 
$current_site;
            
switch_to_blog($current_site->blog_id);
        }
        if (
$timestamp) {
            
wp_unschedule_event($timestamp'wordfence_batchSendAuditEvents');
        }
        if (
$notMainSite) {
            
restore_current_blog();
        }
    }
    
    private function 
_isScheduledAuditEventCronOverdue() {
        if (!
defined('DONOTCACHEDB')) { define('DONOTCACHEDB'true); }
        
$notMainSite is_multisite() && !is_main_site();
        if (
$notMainSite) {
            global 
$current_site;
            
switch_to_blog($current_site->blog_id);
        }
        
        
$overdue false;
        if (
$ts wp_next_scheduled('wordfence_batchSendAuditEvents')) {
            if ((
time() - $ts) > 900) {
                
$overdue $ts;
            }
        }
        
        if (
$notMainSite) {
            
restore_current_blog();
        }
        
        return 
$overdue;
    }
    
    public static function 
checkForUnsentAuditEvents() {
        
$wfdb = new wfDB();
        
$table_wfAuditEvents wfDB::networkTable('wfAuditEvents');
        
$wfdb->queryWrite("UPDATE {$table_wfAuditEvents} SET `state` = 'new', `state_timestamp` = NOW() WHERE `state` = 'sending' AND `state_timestamp` < DATE_SUB(NOW(), INTERVAL 30 MINUTE)");
        
        
$count $wfdb->querySingle("SELECT COUNT(*) AS cnt FROM {$table_wfAuditEvents} WHERE `state` = 'new'");
        if (
$count) {
            
self::shared()->_scheduleSendPendingAuditEvents();
        }
    }
    
    public static function 
trimAuditEvents() {
        
$wfdb = new wfDB();
        
$table_wfAuditEvents wfDB::networkTable('wfAuditEvents');
        
$count $wfdb->querySingle("SELECT COUNT(*) AS cnt FROM {$table_wfAuditEvents}");
        if (
$count 1000) {
            
$wfdb->truncate($table_wfAuditEvents); //Similar behavior to other logged data, assume possible DoS so truncate
        
}
        else if (
$count 100) {
            
$wfdb->queryWrite("DELETE FROM {$table_wfAuditEvents} ORDER BY id ASC LIMIT %d"$count 100);
        }
        else if (
$count 0) {
            
$wfdb->queryWrite("DELETE FROM {$table_wfAuditEvents} WHERE (`state` = 'sending' OR `state` = 'sent') AND `state_timestamp` < DATE_SUB(NOW(), INTERVAL 1 DAY)");
        }
    }
    
    public static function 
hasOverdueEvents() {
        
$wfdb = new wfDB();
        
$table_wfAuditEvents wfDB::networkTable('wfAuditEvents');
        
$count $wfdb->querySingle("SELECT COUNT(*) AS cnt FROM {$table_wfAuditEvents} WHERE `state` = 'new' AND `state_timestamp` < DATE_SUB(NOW(), INTERVAL 2 DAY)");
        return 
$count 0;
    }
    
    
/**
     * Updates the locally-stored audit preview data that is used to populate the audit log page. The preview data is
     * stored in descending order.
     * 
     * @param array $events Structure is [
     *                                         [ //Request 1
     *                                             [<event type>, <timestamp>],
     *                                             [<event type>, <timestamp>],
     *                                             [<event type>, <timestamp>],
     *                                         ],
     *                                         [ //Request 2
     *                                             [<event type>, <timestamp>],
     *                                         ],
     *                                         ...
     *                                     ]
     */
    
protected function _updateAuditPreview($events) {
        
$filtered = array();
        foreach (
$events as $request) {
            
$request array_filter($request, function($e) {
                return 
$e[0] != self::AUDIT_LOG_HEARTBEAT//Don't save heartbeats to the local preview
            
});
            if (!empty(
$request)) {
                
$filtered[] = $request;
            }
        }
        
$events $filtered;
        if (empty(
$events)) { return; }
        
        
$existing wfConfig::get_ser('lastAuditEvents', array());
        if (!
is_array($existing)) {
            
$existing = array();
        }
        
        
$lastAuditEvents array_merge($events$existing);
        
usort($lastAuditEvents, function($a$b) {
            
$aMax array_reduce($a, function($carry$item) {
                return 
max($carry$item[1]);
            }, 
0);
            
$bMax array_reduce($b, function($carry$item) {
                return 
max($carry$item[1]);
            }, 
0);
            if (
$aMax == $bMax) { return 0; }
            return (
$aMax $bMax) ? : -1;
        });
        
        
$lastAuditEvents array_slice($lastAuditEvents0self::AUDIT_LOG_MAX_SAMPLES);
        
wfConfig::set_ser('lastAuditEvents'$lastAuditEvents);
    }
    
    
/**
     * Returns a summary array of recent events for the audit log. The content of this array will be the most recent
     * `AUDIT_LOG_MAX_SAMPLES` requests that were sent (or would have been sent if enabled) to Wordfence Central.
     * 
     * @return array
     */
    
public function auditPreview() {
        
$requests array_filter(wfConfig::get_ser('lastAuditEvents', array()), function($events) {
            return !empty(
$events);
        });
        
        
$data = array();
        if (
is_array($requests)) {
            
$data['requests'] = array();
            foreach (
$requests as $r) {
                
$events array_map(function($e) {
                    return array(
                        
'ts' => $e[1],
                        
'event' => $e[0],
                        
'name' => self::eventName($e[0]),
                        
'category' => self::eventCategory($e[0]),
                    );
                }, 
$r);
                
                
$types array_reduce($events, function($carry$e) { //We'll use the most common category if a request covers multiple
                    
if (!isset($carry[$e['category']])) {
                        
$carry[$e['category']] = 0;
                    }
                    
$carry[$e['category']]++;
                    return 
$carry;
                }, array());
                
asort($typesSORT_NUMERIC);
                
                
$timestamp array_reduce($events, function($carry$e) {
                    if (
$e['ts'] > $carry) {
                        return 
$e['ts'];
                    }
                    return 
$carry;
                }, 
0);
                
                
$data['requests'][] = array(
                    
'ts' => $timestamp,
                    
'category' => array_keys($types),
                    
'events' => $events,
                );
            }
        }
        
        return 
$data;
    }
    
    
/**************************************
     * Utility Functions
     **************************************/
    
    
private function _sanitizeRequestBody() {
        
$input wfUtils::rawPOSTBody();
        
$contentType null;
        if (isset(
$_SERVER['CONTENT_TYPE'])) {
            
$contentType strtolower($_SERVER['CONTENT_TYPE']);
            
$boundary strpos($contentType';');
            if (
$boundary !== false) {
                
$contentType substr($contentType0$boundary);
            }
        }
        
        
$raw null;
        
$response = array('type' => null'parameters' => array(), 'files' => array());
        switch (
$contentType) {
            case 
'application/json':
                try {
                    
$raw json_decode($inputtrue512JSON_OBJECT_AS_ARRAY);
                    
$response['type'] = 'json';
                }
                catch (
Exception $e) {
                    
//Ignore -- can throw on PHP 8+
                
}
                break;
            case 
'multipart/form-data'//PHP has already parsed this into $_POST and $_FILES
                
$response['type'] = 'multipart';
                foreach (
$_FILES as $k => $f) {
                    
$response['files'][] = array(
                        
'name' => $f['name'],
                        
'type' => $f['type'],
                        
'size' => $f['size'],
                        
'error' => $f['error'],
                    );
                }
                
$raw $_POST;
                break;
            default: 
//Typically application/x-www-form-urlencoded
                
if ($input) {
                    
parse_str($input$raw);
                    
$response['type'] = 'urlencoded';
                }
                break;
        }
        
        if (!empty(
$raw)) {
            foreach (
$raw as $k => $v) {
                
$response['parameters'][$k] = null;
                if (
$k == 'action' || //Used in admin-ajax and many other WP calls, typically relevant for auditing and not sensitive
                    
$k == 'id' || //Typically the record ID being affected
                    
$k == 'log' //Authentication username
                
) {
                    
$response['parameters'][$k] = $v;
                }
                
// else if -- future full value captures go here, otherwise we just capture the parameter name for privacy reasons
            
}
            return 
$response;
        }
        
        return 
null;
    }
    
    
/**
     * Returns the desired fields from $userdata for the various user-related hooks, ignoring the rest. Returns null if
     * there is no valid user.
     * 
     * @param array|object|WP_User $userdata
     * @param null|int $user_id Used when provided, otherwise extracted from $userdata when possible
     * @return array|null
     */
    
protected function _sanitizeUserdata($userdata$user_id null) {
        if (
$userdata === null && $user_id !== null) { //May hit this on older WP versions where $userdata wasn't populated by the hook call
            
$userdata get_user_by('ID'$user_id);
        }
        
        
$roles = array();
        if (
$userdata instanceof stdClass) {
            
$user = new WP_User($user_id !== null $user_id : (isset($userdata->ID) ? $userdata->ID 0));
            if (
$user->exists()) {
                
$roles $user->roles;
            }
            
$userdata get_object_vars$userdata );
        } 
        else if (
$userdata instanceof WP_User) {
            
$roles $userdata->roles;
            
$userdata $userdata->to_array();
        }
        else {
            
$user = new WP_User($user_id !== null $user_id : (isset($userdata['ID']) ? $userdata['ID'] : 0));
            if (!
$user) {
                return array(
                    
'user_id' => 0,
                    
'user_login' => '',
                    
'user_roles' => array(),
                );
            }
            
            if (
$user->exists()) {
                
$roles $user->roles;
            }
        }
        
        return array(
            
'user_id' => $user_id !== null $user_id : (isset($userdata['ID']) ? $userdata['ID'] : 0),
            
'user_login' => isset($userdata['user_login']) ? $userdata['user_login'] : '',
            
'user_roles' => $roles,
        );
    }
    
    protected function 
_userdataDiff($userdata1$userdata2) {
        if (
$userdata1 instanceof stdClass) {
            
$userdata1 get_object_vars$userdata1 );
        }
        else if (
$userdata1 instanceof WP_User) {
            
$userdata1 $userdata1->to_array();
        }
        
        if (
$userdata2 instanceof stdClass) {
            
$userdata2 get_object_vars$userdata2 );
        }
        else if (
$userdata2 instanceof WP_User) {
            
$userdata2 $userdata2->to_array();
        }
        
        return 
wfUtils::array_diff_assoc($userdata1$userdata2);
    }
    
    
/**
     * Returns the desired fields for the multisite ignoring the rest.
     * 
     * @param WP_Network|false $network
     * @param WP_Site|false $blog
     * @return array
     */
    
protected function _sanitizeMultisiteData($network$blog) {
        
$result = array();
        
        if (
$network) {
            
$result['network_id'] = $network->id;
            
$result['network_domain'] = $network->domain;
            
$result['network_path'] = $network->path;
            
$result['network_name'] = $network->site_name;
        }
        
        if (
$blog) {
            
$result['blog_id'] = $blog->blog_id;
            
$result['blog_domain'] = $blog->domain;
            
$result['blog_path'] = $blog->path;
            
$result['blog_name'] = $blog->blogname;
        }
        
        return 
$result;
    }
    
    protected function 
_multisiteDiff($blog1$blog2) {
        if (
$blog1 instanceof WP_Site) {
            
$blog1 $this->_sanitizeMultisiteData(false$blog1);
        }
        
        if (
$blog2 instanceof WP_Site) {
            
$blog2 $this->_sanitizeMultisiteData(false$blog2);
        }
        
        return 
wfUtils::array_diff_assoc($blog1$blog2);
    }
    
    
/**
     * Returns the desired fields from an app password record.
     *
     * @param array|object $item
     * @return array
     */
    
protected function _sanitizeAppPassword($item) {
        if (
$item instanceof stdClass) {
            
$item get_object_vars($item);
        }
        
        return array(
            
'uuid' => empty($item['uuid']) ? '<unknown>' $item['uuid'],
            
'app_id' => empty($item['app_id']) ? '<unknown>' $item['app_id'],
            
'name' => empty($item['name']) ? '<empty>' $item['name'],
            
'created' => empty($item['created']) ? $item['created'],
            
'last_used' => empty($item['last_used']) ? null $item['last_used'],
            
'last_ip' => empty($item['last_ip']) ? null $item['last_ip'],
        );
    }
    
    
/**
     * Returns the desired fields from a post record.
     *
     * @param array|object|WP_Post $post
     * @return array
     */
    
protected function _sanitizePost($post) {
        if (
$post instanceof stdClass) {
            
$post get_object_vars($post);
        }
        else if (
$post instanceof WP_Post) {
            
$post $post->to_array();
        }
        
        
$author = isset($post['post_author']) ? get_user_by('ID'$post['post_author']) : null;
        
        
$created null;
        if (isset(
$post['post_date_gmt']) && $post['post_date_gmt'] != '0000-00-00 00:00:00') { //Prefer *_gmt, but sometimes WP doesn't set that
            
$created strtotime($post['post_date_gmt']);
        }
        else if (isset(
$post['post_date'])) {
            
$created strtotime($post['post_date']);
        }
        
        
$modified null;
        if (isset(
$post['post_modified_gmt']) && $post['post_modified_gmt'] != '0000-00-00 00:00:00') { //Prefer *_gmt, but sometimes WP doesn't set that
            
$modified strtotime($post['post_modified_gmt']);
        }
        else if (isset(
$post['post_modified'])) {
            
$modified strtotime($post['post_modified']);
        }
        
        
$sanitized = array(
            
'post_id' => $post['ID'],
            
'author_id' => isset($post['post_author']) ? $post['post_author'] : null,
            
'author' => $author $this->_sanitizeUserdata($author) : null,
            
'title' => isset($post['post_title']) ? $post['post_title'] : null,
            
'created' => $created,
            
'last_modified' => $modified,
            
'type' => isset($post['post_type']) ? $post['post_type'] : 'post',
            
'status' => isset($post['post_status']) ? $post['post_status'] : 'publish',
        );
        if (isset(
$post['post_type']) && $post['post_type'] == wfAuditLogObserversWordPressCoreContent::WP_POST_TYPE_ATTACHMENT) {
            
$sanitized['context'] = get_post_meta($post['ID'], '_wp_attachment_context'true);
        }
        return 
$sanitized;
    }
    
    protected function 
_postDiff($post1$post2) {
        if (
$post1 instanceof stdClass) {
            
$post1 get_object_vars($post1);
        }
        else if (
$post1 instanceof WP_Post) {
            
$post1 $post1->to_array();
        }
        
        if (
$post2 instanceof stdClass) {
            
$post2 get_object_vars($post2);
        }
        else if (
$post2 instanceof WP_Post) {
            
$post2 $post2->to_array();
        }
        
        return 
wfUtils::array_diff_assoc($post1$post2);
    }
    
    
/**
     * Returns whether or not the array of post changes should trigger an event recording. It will return false when
     * there are no changes or when the only changes are innocuous values like post dates.
     * 
     * @param $changes
     * @return bool
     */
    
protected function _shouldRecordPostChanges($changes) {
        if (empty(
$changes) || !is_array($changes)) {
            return 
false;
        }
        
        
$ignored = array('post_date''post_date_gmt''post_modified''post_modified_gmt''menu_order');
        
$test array_filter($changes, function($i) use ($ignored) {
            return !
in_array($i$ignored);
        });
        return !empty(
$test);
    }
    
    protected function 
_extractMultisiteID($option$suffix) {
        global 
$wpdb;
        if (!
is_multisite()) {
            return 
false;
        }
        
        if (
substr($option, -strlen($suffix)) == $suffix) {
            
$option substr($option0strlen($option) - strlen($suffix));
            if (
substr($option0strlen($wpdb->base_prefix)) == $wpdb->base_prefix) {
                
$option substr($optionstrlen($wpdb->base_prefix));
                
$option trim($option'_');
                if (empty(
$option)) {
                    return 
1;
                }
                
                return 
intval($option);
            }
        }
        
        return 
false;
    }
    
    
/**
     * Returns an array containing the installed versions at the time of calling for core and all themes/plugins.
     * 
     * @return array
     */
    
protected function _installedVersions() {
        
$state = array();
        
        require(
ABSPATH WPINC '/version.php'); /** @var string $wp_version */
        
$state['core'] = $wp_version;
        
        if (!
function_exists('get_plugins')) {
            require_once(
ABSPATH '/wp-admin/includes/plugin.php');
        }
        
        
$plugins get_plugins();
        
$state['plugins'] = array_filter(array_map(function($p) { return isset($p['Version']) ? $p['Version'] : null; }, $plugins), function($v) { return $v != null; });
        
        if (!
function_exists('wp_get_themes')) {
            require_once(
ABSPATH '/wp-includes/theme.php');
        }
        
        
$themes wp_get_themes();
        
$state['themes'] = array_filter(array_map(function($t) { return isset($t['Version']) ? $t['Version'] : null; }, $themes), function($v) { return $v != null; });
        
        return 
$state;
    }
    
    
/**
     * Attempts to resolve the given plugin path to the file containing its header. Returns that path if found, otherwise
     * null. Most plugins will simply be .../slug/slug.php, but some are single-file plugins while others have a 
     * non-standard PHP file containing the header.
     * 
     * Based on `get_plugins()`.
     * 
     * @param string $path
     * @return string|null
     */
    
protected function _resolvePlugin($path) {
        if (
is_dir($path)) {
            
$scanner = @opendir($path);
            
            if (
$scanner) {
                while ((
$subfile readdir($scanner)) !== false) {
                    if (
preg_match('/^\./i'$subfile)) {
                        continue;
                    }
                    else if (
preg_match('/\.php$/i'$subfile)) {
                        if (!
is_readable($path DIRECTORY_SEPARATOR $subfile)) {
                            continue;
                        }
                        
                        
$plugin_data get_plugin_data($path DIRECTORY_SEPARATOR $subfilefalsefalse);
                        if (!empty(
$plugin_data['Name'])) {
                            return 
$path DIRECTORY_SEPARATOR $subfile;
                        }
                    }
                }
                
                
closedir($scanner);
            }
        }
        else if (
preg_match('/\.php$/i'$path) && is_readable($path)) {
            
$plugin_data get_plugin_data($pathfalsefalse);
            if (!empty(
$plugin_data['Name'])) {
                return 
$path;
            }
        }
        
        return 
null;
    }
    
    
/**
     * Returns data for the plugin at $path if possible, otherwise null.
     * 
     * @param string $path
     * @return array|null
     */
    
protected function _getPlugin($path) {
        
$original $this->_getState('upgrader_pre_install.versions'0);
        
$raw get_plugin_data($path);
        if (
$raw) {
            
$data = array();
            foreach (
$raw as $f => $v) {
                
$k strtolower(preg_replace('/\s+/''_'$f)); //Translates all headers: Plugin Name -> plugin_name
                
$data[$k] = $v;
            }
            
            
$base plugin_basename($path);
            if (
$original && isset($original['plugins'][$base])) {
                
$data['previous_version'] = $original['plugins'][$base];
            }
            
            return 
$data;
        }
        return 
null;
    }
    
    
/**
     * Returns data for the theme if possible, otherwise null.
     * 
     * @param WP_Theme|string $theme_or_path
     * @return array|null
     */
    
protected function _getTheme($theme_or_path) {
        
$original $this->_getState('upgrader_pre_install.versions'0);
        
        if (
$theme_or_path instanceof WP_Theme) {
            
$theme $theme_or_path;
        }
        else {
            
$theme wp_get_theme(basename($theme_or_path), dirname($theme_or_path));
        }
        
        if (
$theme) {
            
$fields = array(
                
'Name',
                
'ThemeURI',
                
'Description',
                
'Author',
                
'AuthorURI',
                
'Version',
                
'Template',
                
'Status',
                
'Tags',
                
'TextDomain',
                
'DomainPath',
                
'RequiresWP',
                
'RequiresPHP',
                
'UpdateURI',
            );
            
$data = array();
            foreach (
$fields as $f) {
                
$k strtolower(preg_replace('/\s+/''_'$f));
                
$data[$k] = $theme->display($f);
            }
            
            
$base $theme->get_stylesheet();
            if (
$original && isset($original['themes'][$base])) {
                
$data['previous_version'] = $original['themes'][$base];
            }
            
            return 
$data;
        }
        return 
null;
    }
}

class 
wfAuditLogSendFailedException extends Exception { }

All system for education purposes only. For more tools: Telegram @jackleet

Mr.X Private Shell

Logo
-
New File | New Folder
Command
SQL