$data ) { $plugins[ $file ] = array( 'name' => $data['Name'], 'version' => $data['Version'], 'active' => in_array( $file, $active_plugins, true ) || ( is_multisite() && is_plugin_active_for_network( $file ) ), ); } $stylesheet = get_stylesheet(); $template = get_template(); $themes = array(); foreach ( wp_get_themes() as $slug => $theme ) { $themes[ $slug ] = array( 'name' => $theme->get( 'Name' ), 'version' => $theme->get( 'Version' ), 'active' => ( $slug === $stylesheet || $slug === $template ), ); } return array( 'captured_at' => current_time( 'mysql', true ), 'core' => array( 'wordpress' => null !== self::$updated_core_version ? self::$updated_core_version : $wp_version, 'php' => PHP_VERSION, 'database' => $wpdb->db_server_info(), ), 'plugins' => $plugins, 'themes' => $themes, ); } /** * Last stored snapshot. * * @return array|null */ public static function get_stored() { $stored = get_option( self::OPTION ); return is_array( $stored ) ? $stored : null; } /** * Stores a snapshot as the new baseline. * * @param array $snapshot Snapshot from capture(). */ public static function store( array $snapshot ) { update_option( self::OPTION, $snapshot, false ); } /** * Compares two snapshots. * * @param array $old Previous snapshot. * @param array $new Current snapshot. * @return array[] List of changes, each with kind, component, key, name, from, to. */ public static function diff( array $old, array $new ) { $changes = array(); $core_labels = array( 'wordpress' => 'WordPress', 'php' => 'PHP', 'database' => __( 'Database server', 'maintained-log' ), ); foreach ( $core_labels as $key => $label ) { $from = isset( $old['core'][ $key ] ) ? (string) $old['core'][ $key ] : ''; $to = isset( $new['core'][ $key ] ) ? (string) $new['core'][ $key ] : ''; if ( $from !== $to && '' !== $to ) { $changes[] = array( 'kind' => 'updated', 'component' => 'wordpress' === $key ? 'core' : 'server', 'key' => $key, 'name' => $label, 'from' => $from, 'to' => $to, ); } } foreach ( array( 'plugins' => 'plugin', 'themes' => 'theme', ) as $group => $component ) { $before = isset( $old[ $group ] ) ? (array) $old[ $group ] : array(); $after = isset( $new[ $group ] ) ? (array) $new[ $group ] : array(); foreach ( $after as $key => $item ) { if ( ! isset( $before[ $key ] ) ) { $changes[] = self::change( 'installed', $component, $key, $item, '', $item['version'] ); continue; } $prev = $before[ $key ]; if ( (string) $prev['version'] !== (string) $item['version'] ) { $kind = version_compare( $item['version'], $prev['version'], '<' ) ? 'downgraded' : 'updated'; $changes[] = self::change( $kind, $component, $key, $item, $prev['version'], $item['version'] ); } if ( (bool) $prev['active'] !== (bool) $item['active'] ) { $changes[] = self::change( $item['active'] ? 'activated' : 'deactivated', $component, $key, $item, '', $item['version'] ); } } foreach ( $before as $key => $item ) { if ( ! isset( $after[ $key ] ) ) { $changes[] = self::change( 'removed', $component, $key, $item, $item['version'], '' ); } } } return $changes; } /** * Looks for changes made outside WordPress (hosting panel, FTP, deployments) * when someone opens the log or the dashboard, at most every two minutes, * instead of waiting for the twice-daily job. */ public static function maybe_detect_external() { // A core update in progress would be seen half-applied. if ( get_transient( 'maintained_log_recent_detect' ) || get_option( 'core_updater.lock' ) ) { return; } set_transient( 'maintained_log_recent_detect', 1, 2 * MINUTE_IN_SECONDS ); self::detect_changes( Log::SOURCE_EXTERNAL ); } /** * Captures, diffs against the stored snapshot, logs changes and stores. * * @param string $source Log source to attribute the changes to. * @param array $context Extra details merged into each entry. * @return array[] Changes found. */ public static function detect_changes( $source, array $context = array() ) { if ( ! function_exists( 'wp_clean_plugins_cache' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } wp_clean_plugins_cache( false ); if ( function_exists( 'wp_clean_themes_cache' ) ) { wp_clean_themes_cache( false ); } $current = self::capture(); $stored = self::get_stored(); if ( ! $stored ) { self::store( $current ); return array(); } $changes = self::diff( $stored, $current ); foreach ( $changes as $change ) { Log::add( array( 'type' => Log::TYPE_UPDATE, 'category' => $change['component'], 'status' => 'downgraded' === $change['kind'] ? Log::STATUS_WARNING : Log::STATUS_SUCCESS, 'title' => self::describe( $change ), 'details' => array_merge( $change, $context ), 'source' => $source, // Nobody in this request made a change found outside WordPress. 'user_id' => Log::SOURCE_EXTERNAL === $source ? 0 : get_current_user_id(), ) ); } self::store( $current ); return $changes; } /** * Human readable sentence for a change. * * @param array $change Change from diff(). * @return string */ public static function describe( array $change ) { switch ( $change['kind'] ) { case 'updated': /* translators: 1: component name, 2: old version, 3: new version. */ return sprintf( __( '%1$s updated from %2$s to %3$s', 'maintained-log' ), $change['name'], $change['from'], $change['to'] ); case 'downgraded': /* translators: 1: component name, 2: old version, 3: new version. */ return sprintf( __( '%1$s rolled back from %2$s to %3$s', 'maintained-log' ), $change['name'], $change['from'], $change['to'] ); case 'installed': /* translators: 1: component name, 2: version. */ return sprintf( __( '%1$s %2$s installed', 'maintained-log' ), $change['name'], $change['to'] ); case 'removed': /* translators: 1: component name, 2: version. */ return sprintf( __( '%1$s %2$s removed', 'maintained-log' ), $change['name'], $change['from'] ); case 'activated': /* translators: %s: component name. */ return sprintf( __( '%s activated', 'maintained-log' ), $change['name'] ); case 'deactivated': /* translators: %s: component name. */ return sprintf( __( '%s deactivated', 'maintained-log' ), $change['name'] ); } return $change['name']; } /** * Builds a change record. * * @param string $kind Change kind. * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @param array $item Snapshot item. * @param string $from Old version. * @param string $to New version. * @return array */ private static function change( $kind, $component, $key, array $item, $from, $to ) { return array( 'kind' => $kind, 'component' => $component, 'key' => $key, 'name' => $item['name'], 'from' => (string) $from, 'to' => (string) $to, ); } }