'', 'failed' => array(), 'successful' => array(), ); private $settings; private $items; const PLUGINS_PACKAGE_URL = 'https://downloads.wordpress.org/plugin/%s.%s.zip'; const THEMES_PACKAGE_URL = 'https://downloads.wordpress.org/theme/%s.%s.zip'; const WP_PACKAGE_URL = 'https://downloads.wordpress.org/release/%s/wordpress-%s.zip'; const WP_PACKAGE_NO_CONTENT_URL = 'https://downloads.wordpress.org/release/wordpress-%s-no-content.zip'; const ERROR_DESCRIPTION = 'Error description --> '; const WP_UPGRADER_FILE_PATH = 'wp-admin/includes/class-wp-upgrader.php'; const WP_UPDATE_FILE_PATH = 'wp-admin/includes/update.php'; const WP_FILE_SYSTEM_PATH = 'wp-admin/includes/file.php'; const WP_MISC_PATH = 'wp-admin/includes/misc.php'; const LOG_DIVIDER = '#################################'; public function __construct() { $this->settings = new OCVMSettings(); // ??? } /** * Get FQN of plugin by slug * @param $slug string Plugin's PHP file name (without extension) * @return string directory/plugin_file.php */ protected function find_plugin_for_slug( $slug ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_files = get_plugins( '/' . $slug ); if ( ! $plugin_files ) { return ''; } $plugin_files = array_keys( $plugin_files ); return $slug . '/' . reset( $plugin_files ); } /** * Prepare update attempt records */ public function prepareAttempt( $type, $slug, $new_ver, $itemFQN, $item_data ) { $attempt = array( 'type' => $type, 'slug' => $slug, 'dir' => $itemFQN, 'new_version' => $new_ver, 'time' => time(), ); if ( ! empty( $item_data ) ) { $attempt['name'] = $item_data['Name']; $attempt['old_version'] = $item_data['Version']; } // get latest state from DB $settings = $this->settings->get(); if ( 'wp' === $type ) { $vuls = $settings['vulnerabilities'][ $type ]['vulnerabilities']; } else { $vuls = $settings['vulnerabilities'][ $type ][ $slug ]['vulnerabilities']; } foreach ( $vuls as $v ) { // modified to manipulate the vuln_type string and url recieved from new API $desc = 'wp_vul_' . strtolower( str_replace( array( ' ', '(', ')' ), array( '_', '', '' ), $v['vuln_type'] ) ); if ( $this->vulTranslation( $desc ) !== '' ) { $attempt['vuls'][ $v['id'] ]['id'] = $this->vulTranslation( $desc ); } elseif ( isset( $attempt['vuls'][ $v['id'] ]['id'] ) || isset( $v['description'] ) ) { $attempt['vuls'][ $v['id'] ]['id'] = $v['description']; } else { $attempt['vuls'][ $v['id'] ]['id'] = __( 'wp_vul_unknown', 'onecom-wp' ); } $attempt['vuls'][ $v['id'] ]['url'] = $v['url']; } return $attempt; } /** * Update item records after attempting their update * @param string $type * @param string $slug * @param array $attempt * @return void */ public function updateItemRecords( $type, $slug, $attempt = array(), $ver = '' ): void { // seat belt if ( empty( $type ) || empty( $slug ) ) { error_log( "Failed to update records because either 'itemType' or 'itemSlug' or both are empty." ); return; } // get latest state from DB $settings = $this->settings->get(); // Prepare consistent structure to make use of existing iterate/push functions $log_item_data = array(); // remove the item from vulnerabilities list. in case of WP, clear the array if ( 'wp' === $type ) { // Before clearing vul, Add fixed item to VM log at one less level as compared to themes/plugins $log_item_data[ $type ] = $settings['vulnerabilities'][ $type ]; $log_item_data[ $type ]['log_item_result'] = 'Auto-updated'; $log_item_data[ $type ]['log_latest_version'] = $ver; // clear wp vulnerability $settings['vulnerabilities'][ $type ] = array(); } else { $failed_vm = array(); if ( isset( $attempt['failed'] ) && is_array( $attempt['failed'] ) && ! empty( $attempt['failed'] ) ) { foreach ( $attempt['failed'] as $failed_array ) { $slug = strtolower( $failed_array['slug'] ); if ( isset( $settings['vulnerabilities'][ $type ][ $slug ] ) ) { $failed_vm[ $slug ] = $settings['vulnerabilities'][ $type ][ $slug ]; unset( $settings['vulnerabilities'][ $type ][ $slug ] ); } } } /** * Add relevant information (type) inside vul item array itself * Then, Fixed vul item array is ready to prepare in $log_item_data */ $settings['vulnerabilities'][ $type ][ $slug ]['item_type'] = $type; $log_item_data[ $type ][ $slug ] = $settings['vulnerabilities'][ $type ][ $slug ]; $log_item_data[ $type ][ $slug ]['log_item_result'] = 'Auto-updated'; $log_item_data[ $type ][ $slug ]['log_latest_version'] = $ver; // remove the item for which we have attempted an update unset( $settings['vulnerabilities'][ $type ][ $slug ] ); $settings['vulnerabilities'][ $type ] = array_merge( $settings['vulnerabilities'][ $type ], $failed_vm ); } // Fixed vulnerability item is Ready to Iterate (to extract all vuls from item) for VM log push error_log( '======= Fixed vulnerability item is ready to iterate for log push =======' ); $history_log_obj = new OCVMHistoryLog(); $history_log_obj->iterateVulnerabilitiesForLog( $log_item_data ); // save the current attempt of item updates in database if ( ! empty( $attempt ) ) { if ( ! empty( $this->updateAttempt ) ) { foreach ( $this->updateAttempt as $types => &$attempts ) { if ( $types === 'time' ) { continue; } // update attempt counts in transient $this->add_update_attempts_count( $types, $attempts ); foreach ( $attempts as &$attempt ) { // stored with attempts to prevent dupliate counts in the same attempt $attempt['attempt_stored'] = true; } } } // Remove the failed attempts if ( isset( $this->updateAttempt['failed'] ) ) { unset( $this->updateAttempt['failed'] ); } $settings['update_attempts'] = $this->updateAttempt; } $this->settings->update( $settings ); } /** Update single plugin * @param $type string Type of item (plugins) * @param $slug string Filename of the item * @param $ver string Desired version for update * @return void */ public function updateTypePlugins( $type, $slug, $ver, $plugin_package ) { // get all details of the plugin using slug $pluginFQN = $this->find_plugin_for_slug( $slug ); if ( empty( $pluginFQN ) ) { error_log( "Skipping update by rule...[Plugin '{$slug}' no longer exists]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug ); return; } if ( false === is_plugin_active( $pluginFQN ) ) { error_log( "Skipping update by rule...[Plugin '{$slug}' is no longer active]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug ); return; } // load plugin functions to get plugin headers if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // plugin headers by plugin dir // e.g., /../../../../../test-plugin/test-plugin.php $pluginData = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . $pluginFQN ); // exit if fixed_version and current versions are same. if ( (string) $ver === (string) $pluginData['Version'] ) { error_log( "Skipping update by rule...[Plugin '{$slug}' is already at the 'fixed' version]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug, array(), $ver ); return; } // check if the plugin update with same versions was updated in last 24 hours to prevent repetitive attempts if ( $this->validate_against_last_update( $slug, $type, $ver, $pluginData['Version'] ) ) { return; } // record update attempt details $attempt = $this->prepareAttempt( $type, $slug, $ver, $pluginFQN, $pluginData ); error_log( self::LOG_DIVIDER ); error_log( "##### starting update of {$slug}" ); error_log( self::LOG_DIVIDER ); $pluginObj = new stdClass(); $pluginObj->slug = $slug; $pluginObj->plugin = $pluginFQN; $pluginObj->new_version = $ver; $pluginObj->package = ( $plugin_package !== '' ) ? $plugin_package : sprintf( self::PLUGINS_PACKAGE_URL, $slug, $ver ); $current = get_site_transient( 'update_plugins' ); if ( ! is_object( $current ) ) { $current = new stdClass(); } $current->response[ $pluginFQN ] = $pluginObj; set_site_transient( 'update_plugins', $current ); require_once ABSPATH . self::WP_FILE_SYSTEM_PATH; require_once ABSPATH . self::WP_UPGRADER_FILE_PATH; require_once ABSPATH . self::WP_UPDATE_FILE_PATH; require_once ABSPATH . self::WP_MISC_PATH; wp_cache_flush(); $upgrader = new Plugin_Upgrader(); $result = $upgrader->upgrade( $pluginFQN ); // modified to handle the null result in case of failed plugin downloads may be due to the non-working url of plugin zip if ( is_wp_error( $result ) || ! $result ) { //TODO: handle failed updates - perhaps send an email and remove records from DB // keep status of update $this->updateAttempt['failed'][] = $attempt; error_log( 'Error occurred during update of plugin --> ' . $pluginFQN ); // added for the case when update fails may be due to the non-working url of plugin zip $err_description = ! is_null( $result ) ? json_encode( $result->get_error_message() ) : 'Plugin download failed for some reason.'; error_log( self::ERROR_DESCRIPTION . $err_description ); } else { $pluginData = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . $pluginFQN ); if ( (string) $ver <= (string) $pluginData['Version'] ) { error_log( '######### Plugin version verified update was successful #######' ); // keep status of update $this->updateAttempt['successful'][] = $attempt; error_log( "Updated {$slug} successfully --> " . $pluginFQN ); } else { $this->updateAttempt['failed'][] = $attempt; error_log( '######### Plugin version verification check failed adding to failed attempts #########' ); } } // update records for this item if successfully updated $this->updateItemRecords( $type, $slug, $this->updateAttempt, $ver ); } /** * Update single theme * @param $type string Type of item (themes) * @param $slug string Filename of the item * @param $ver string Desired version for update * @return void */ public function updateTypeThemes( $type, $slug, $ver, $theme_package ) { // theme headers $themeData = wp_get_theme( $slug ); // check if theme exists if ( false === $themeData->exists() ) { error_log( "Skipping update by rule...[Theme '{$slug}' no longer exists]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug ); return; } // check if theme is active if ( get_site_option( 'template' ) !== $themeData->template ) { error_log( "Skipping update by rule...[Theme '{$slug}' is no longer active]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug ); return; } // exit if fixed_version is equal to current versions are same. if ( (string) $ver === (string) $themeData->get( 'Version' ) ) { error_log( "Skipping update by rule...[Theme '{$slug}' is already at the 'fixed' version]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug, array(), $ver ); return; } // check if the theme update with same versions was updated in last 24 hours to prevent repetitive attempts if ( $this->validate_against_last_update( $slug, $type, $ver, $themeData->get( 'Version' ) ) ) { return; } // record update attempt details $attempt = $this->prepareAttempt( $type, $slug, $ver, $themeData->stylesheet, $themeData ); error_log( self::LOG_DIVIDER ); error_log( "##### starting update of {$slug}" ); error_log( self::LOG_DIVIDER ); $themeObj['theme'] = $themeData->template; $themeObj['new_version'] = $ver; $themeObj['package'] = ( $theme_package !== '' ) ? $theme_package : sprintf( self::THEMES_PACKAGE_URL, $slug, $ver ); $current = get_site_transient( 'update_themes' ); if ( $current !== false ) { $current->response[ $themeData->template ] = $themeObj; } set_site_transient( 'update_themes', $current ); require_once ABSPATH . self::WP_FILE_SYSTEM_PATH; require_once ABSPATH . self::WP_UPGRADER_FILE_PATH; require_once ABSPATH . self::WP_UPDATE_FILE_PATH; require_once ABSPATH . self::WP_MISC_PATH; wp_cache_flush(); $upgrader = new Theme_Upgrader(); $result = $upgrader->upgrade( $themeData->template ); if ( is_wp_error( $result ) || ! $result ) { //TODO: handle failed updates - perhaps send an email and remove records from DB // keep status of update $this->updateAttempt['failed'][] = $attempt; error_log( 'Error occurred during update of theme --> ' . $themeData->template ); $err_description = ! is_null( $result ) ? json_encode( $result->get_error_message() ) : 'Theme download failed for some reason.'; error_log( self::ERROR_DESCRIPTION . $err_description ); } else { // theme headers $themeData = wp_get_theme( $slug ); if ( (string) $ver === (string) $themeData->get( 'Version' ) ) { error_log( '######### Theme version verified update was successful #######' ); // keep status of update $this->updateAttempt['successful'][] = $attempt; error_log( "Updated {$slug} successfully --> " . $themeData->template ); } else { $this->updateAttempt['failed'][] = $attempt; error_log( '######### Theme version verification check failed adding to failed attempts #########' ); } } $this->updateItemRecords( $type, $slug, $this->updateAttempt, $ver ); } /** * Update single theme * @param $type string Type of item (themes) * @param $slug string Filename of the item * @param $ver string Desired version for update * @return void */ public function updateTypeWp( $type, $slug, $ver ) { global $wpdb, $wp_version; if ( version_compare( $wp_version, $ver ) >= 0 ) { error_log( "Skipping update by rule...[WP core is already at a greater or equal version than provided version i.e., {$wp_version} >= {$ver}]" ); // (remove this item from database) update records for this item $this->updateItemRecords( $type, $slug, array(), $wp_version ); return; } $item_data = array( 'Name' => 'WordPress core', 'Version' => $wp_version, ); // record update attempt details $attempt = $this->prepareAttempt( $type, $slug, $ver, '', $item_data ); error_log( 'attempt for WP ==> ' . json_encode( $attempt ) ); error_log( self::LOG_DIVIDER ); error_log( "##### starting update to WP core v{$ver}" ); error_log( self::LOG_DIVIDER ); $locale = apply_filters( 'core_version_check_locale', get_locale() ); $wp_obj = new stdClass(); $wp_obj->response = 'upgrade'; $wp_obj->download = sprintf( self::WP_PACKAGE_URL, $locale, $ver ); $wp_obj->locale = $locale; $wp_obj->current = $wp_version; $wp_obj->version = $wp_version; $wp_obj->php_version = phpversion(); $wp_obj->mysql_version = preg_replace( '/[^0-9.].*/', '', $wpdb->db_version() ); $wp_obj->new_bundled = $wp_version; $package = new stdClass(); $package->full = $wp_obj->download; $package->no_content = sprintf( self::WP_PACKAGE_NO_CONTENT_URL, $ver ); $package->new_bundled = ''; $package->partial = ''; $package->rollback = ''; $wp_obj->packages = $package; $current = get_site_transient( 'update_core' ); //create a generic empty class if $current is not an object if ( ! is_object( $current ) ) { $current = new stdClass(); } $current->updates = array( $wp_obj ); set_site_transient( 'update_core', $current ); require_once ABSPATH . self::WP_FILE_SYSTEM_PATH; require_once ABSPATH . self::WP_UPGRADER_FILE_PATH; require_once ABSPATH . self::WP_UPDATE_FILE_PATH; require_once ABSPATH . self::WP_MISC_PATH; wp_cache_flush(); $upgrader = new Core_Upgrader(); $result = $upgrader->upgrade( $wp_obj, array( 'attempt_rollback' => true ) ); if ( is_wp_error( $result ) || ! $result ) { //TODO: handle failed updates - perhaps send an email and remove records from DB // keep status of update $this->updateAttempt['failed'][] = $attempt; error_log( "Error occurred during update of WP core --> {$ver}" ); error_log( self::ERROR_DESCRIPTION . json_encode( $result->get_error_message() ) ); } else { // keep status of update $this->updateAttempt['successful'][] = $attempt; error_log( "Updated WP core v{$ver} successfully" ); } // update records for this item if successfully updated $this->updateItemRecords( $type, $slug, $this->updateAttempt, $ver ); } /** * Update single item * @param $type string Type of item (plugins/themes/core) * @param $slug string Filename of the item * @param $ver string Desired version for update * @return void */ public function updateSingle( $type, $slug, $ver, $package_url = '' ): void { // call update method based on item type if ( 'plugins' === $type || 'themes' === $type ) { call_user_func( array( $this, 'updateType' . ucfirst( $type ) ), $type, $slug, $ver, $package_url ); } else { call_user_func( array( $this, 'updateType' . ucfirst( $type ) ), $type, $slug, $ver ); } // alternate dynamic function call //[$this, "updateType".ucfirst($type)]($type,$slug,$ver); // select suitable WP's update method based on received params // remove entry for this item from Settings in DB. So that its admin notice will go away. // add slug in updateState array. } /** * Auto update items */ public function updateItems() { $items = $this->settings->get(); $this->items = $items['vulnerabilities']; $this->updateAttempt['time'] = time(); foreach ( $this->items as $type => $vuls ) { if ( 'wp' === $type ) { $this->updateSingle( $type, $type, $vuls['fixed_in'] ); continue; } foreach ( $vuls as $slug => $item ) { // skip if fix version not present if ( empty( $item['fixed_in'] ) ) { continue; } $get_latest_ver = $this->get_wp_latest_plugin_version( $slug, $type ); $patched_in_range = false; foreach ( $item['vulnerabilities'] as $vuln ) { if ( is_array( $vuln['patched_in_ranges'] ) && ! empty( $vuln['patched_in_ranges'] ) ) { $patched_in_range = true; break; } } if ( $type === 'plugins' && 'woocommerce' === $slug ) { $this->updateSingle( $type, $slug, $item['fixed_in'] ); } elseif ( false !== $get_latest_ver && version_compare( $item['fixed_in'], $get_latest_ver['version'], '>' ) ) { $update_transient = get_site_transient( 'update_plugins' ); $pluginFQN = $this->find_plugin_for_slug( $slug ); if ( $update_transient && ! empty( $pluginFQN ) && isset( $update_transient->response[ $pluginFQN ]->new_version ) ) { $item['fixed_in'] = $update_transient->response[ $pluginFQN ]->new_version; } $this->updateSingle( $type, $slug, $item['fixed_in'] ); } elseif ( $patched_in_range ) { $this->updateSingle( $type, $slug, $item['fixed_in'] ); } elseif ( $get_latest_ver !== false ) { $this->updateSingle( $type, $slug, $get_latest_ver['version'], $get_latest_ver['download_link'] ); } else { $this->updateSingle( $type, $slug, $item['fixed_in'] ); } } } // send email with param updateState array } /** * Get the latest version of a WordPress plugin or theme. * * This function retrieves the latest version of a WordPress plugin or theme * from the WordPress.org API based on the provided slug and type. * * @param string $slug The slug of the plugin or theme. * @param string $type The type of the resource ('plugin' or 'theme'). * * @return string|false The latest version of the resource as a string, * */ public function get_wp_latest_plugin_version( $slug, $type ) { // Ensure the slug is provided. if ( empty( $slug ) ) { return false; } // Define the API endpoint based on the type. $api_endpoint = ( $type === 'themes' ) ? "https://api.wordpress.org/themes/info/1.1/?action=theme_information&request[slug]={$slug}" : "https://api.wordpress.org/plugins/info/1.0/{$slug}.json"; // Make an HTTP request to the API. $response = wp_safe_remote_get( $api_endpoint ); // Get the HTTP status code from the response. $status_code = wp_remote_retrieve_response_code( $response ); // Check if the status code is not 200 (OK). if ( $status_code !== 200 ) { return false; // Request was not successful. } // Check if the request was successful but the response is empty. if ( is_wp_error( $response ) || empty( $response['body'] ) ) { return false; // Empty or failed response. } // Parse the JSON response. $data = json_decode( $response['body'], true ); // Check if JSON decoding was successful and if the data contains version information. if ( $data && isset( $data['version'] ) && isset( $data['download_link'] ) ) { $plugin_info = array( 'version' => $data['version'], 'download_link' => $data['download_link'], ); return $plugin_info; // Return version and download link in an array. } else { return false; // Version information not found in the response. } } /** * @param $settings * @param $type * function to add/increment the update attempts count to the transients, which can be further used to limit repetitive attempts * @return void */ public function add_update_attempts_count( $type, $settings ) { // Implemented to add the attempt counts with the update attempts $last_update_attempts = array(); if ( isset( $settings ) && is_array( $settings ) && ! empty( $settings ) ) { unset( $settings['time'] ); $last_update = get_site_transient( 'ocvm_last_update_attempt' ); if ( ! $last_update ) { foreach ( $settings as $success_attempt ) { unset( $success_attempt['vuls'] ); $success_attempt['attempt_count'] = 1; $last_update_attempts[ $type ][] = $success_attempt; } } else { $last_update_attempts = json_decode( $last_update, true ); if ( $last_update_attempts !== null ) { foreach ( $settings as $success_attempt ) { if ( isset( $success_attempt['attempt_stored'] ) && $success_attempt['attempt_stored'] === true ) { continue; } $found = false; if ( isset( $last_update_attempts[ $type ] ) ) { foreach ( $last_update_attempts[ $type ] as &$attempt ) { if ( $success_attempt['slug'] === $attempt['slug'] && $success_attempt['new_version'] === $attempt['new_version'] && $success_attempt['old_version'] === $attempt['old_version'] ) { // Increment attempt count $attempt['attempt_count'] = ( $attempt['attempt_count'] ?? 0 ) + 1; $found = true; } } } // If matching attempt not found, add a new attempt if ( ! $found ) { $new_attempt = $success_attempt; unset( $new_attempt['vuls'] ); $new_attempt['attempt_count'] = 1; $last_update_attempts[ $type ][] = $new_attempt; } } } } // Store updated last_update_attempts array in transient set_site_transient( 'ocvm_last_update_attempt', json_encode( $last_update_attempts ), 24 * HOUR_IN_SECONDS ); } } /** * @param $slug * @param $type * @param $new_version * @param $old_version * function to verify the current attempt with the last successful attempt to prevent repetitive updates & notifications in case of fake success * @return bool */ public function validate_against_last_update( $slug, $type, $new_version, $old_version ) { $last_update = get_site_transient( 'ocvm_last_update_attempt' ); if ( $last_update ) { $last_update_attempts = json_decode( $last_update, true ); if ( $last_update_attempts !== null ) { foreach ( $last_update_attempts as $attempt_type ) { foreach ( $attempt_type as $attempt ) { if ( $slug === $attempt['slug'] && $new_version === $attempt['new_version'] && $old_version === $attempt['old_version'] && ( $attempt['attempt_count'] >= 3 ) ) { $type = ucfirst( $type ); error_log( "Skipping update by rule...[$type '{$slug}' update for the same version was attempted more than 3 times in last 24 hours ]" ); return true; } } } } } return false; } }