capabilities rows, orphan rows included. * * @param int $cursor Last umeta_id read. * @return array{items:UserEntry[], cursor:int, done:bool} */ public static function users( $cursor ) { global $wpdb; $key = $wpdb->get_blog_prefix() . 'capabilities'; $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- read-only pass over raw rows, orphans included. $wpdb->prepare( "SELECT m.umeta_id, m.user_id, m.meta_value, u.ID AS uid, u.user_login, u.user_registered FROM {$wpdb->usermeta} m LEFT JOIN {$wpdb->users} u ON u.ID = m.user_id WHERE m.meta_key = %s AND m.umeta_id > %d ORDER BY m.umeta_id ASC LIMIT %d", $key, $cursor, self::USERS_PAGE ), ARRAY_A ); $items = array(); foreach ( (array) $rows as $row ) { $cursor = (int) $row['umeta_id']; $caps = maybe_unserialize( $row['meta_value'] ); $exists = null !== $row['uid']; $items[] = UserEntry::fromArray( array( 'id' => (int) $row['user_id'], 'login' => $exists ? (string) $row['user_login'] : '', 'registered' => $exists ? (string) $row['user_registered'] : '', 'capabilities' => is_array( $caps ) ? $caps : array(), 'isSuperAdmin' => $exists && is_multisite() && is_super_admin( (int) $row['user_id'] ), 'exists' => $exists, ) ); } $done = count( (array) $rows ) < self::USERS_PAGE; if ( $done ) { $items = array_merge( $items, self::super_admins_without_capabilities( $key ) ); } return array( 'items' => $items, 'cursor' => $cursor, 'done' => $done, ); } /** * Multisite: super-admins of the network option site_admins that have no * capabilities row on the main site (D-07, grant_super_admin(), wpmu_create_user()). * * @param string $key Capabilities meta key of the main site. * @return UserEntry[] */ private static function super_admins_without_capabilities( $key ) { if ( ! is_multisite() ) { return array(); } $out = array(); foreach ( (array) get_super_admins() as $login ) { $user = get_user_by( 'login', (string) $login ); if ( ! $user || metadata_exists( 'user', (int) $user->ID, $key ) ) { continue; } $out[] = UserEntry::fromArray( array( 'id' => (int) $user->ID, 'login' => (string) $user->user_login, 'registered' => (string) $user->user_registered, 'capabilities' => array(), 'isSuperAdmin' => true, 'exists' => true, ) ); } return $out; } /** * One page of contents of public types, statuses of the SPEC excluded. * * @param int $cursor Last post ID read. * @return array{items:ContentEntry[], cursor:int, done:bool} */ public static function contents( $cursor ) { global $wpdb; $types = array_values( get_post_types( array( 'public' => true ) ) ); if ( ! $types ) { return array( 'items' => array(), 'cursor' => $cursor, 'done' => true, ); } $excluded = Assumptions::CONTENT_EXCLUDED_STATUSES; $in_types = implode( ', ', array_fill( 0, count( $types ), '%s' ) ); $in_excl = implode( ', ', array_fill( 0, count( $excluded ), '%s' ) ); $sql = "SELECT ID, post_type, post_status, post_title, post_author, post_date, post_modified, SUBSTRING(post_content, 1, %d) AS post_content FROM {$wpdb->posts} WHERE post_type IN ($in_types) AND post_status NOT IN ($in_excl) AND ID > %d ORDER BY ID ASC LIMIT %d"; $values = array_merge( array( Assumptions::CONTENT_MAX_BYTES ), $types, $excluded, array( $cursor, self::CONTENTS_PAGE ) ); $rows = $wpdb->get_results( $wpdb->prepare( $sql, $values ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- placeholders built from counts only, values prepared. $items = array(); $authors = array(); foreach ( (array) $rows as $row ) { $cursor = (int) $row['ID']; $author = (int) $row['post_author']; if ( ! array_key_exists( $author, $authors ) ) { $user = $author > 0 ? get_userdata( $author ) : false; $authors[ $author ] = $user ? (string) $user->user_registered : null; } $items[] = ContentEntry::fromArray( array( 'id' => $cursor, 'type' => (string) $row['post_type'], 'status' => (string) $row['post_status'], 'title' => (string) $row['post_title'], 'authorId' => $author, 'authorRegistered' => $authors[ $author ], 'postDate' => (string) $row['post_date'], 'postModified' => (string) $row['post_modified'], 'content' => (string) $row['post_content'], ) ); } return array( 'items' => $items, 'cursor' => $cursor, 'done' => count( (array) $rows ) < self::CONTENTS_PAGE, ); } /** * Every scheduled event, with the code attached to its hook on this page load. * * @return CronEvent[] */ public static function cron() { $crons = _get_cron_array(); $out = array(); $cache = array(); foreach ( (array) $crons as $timestamp => $hooks ) { if ( ! is_array( $hooks ) ) { continue; } foreach ( $hooks as $hook => $events ) { if ( ! isset( $cache[ $hook ] ) ) { $cache[ $hook ] = self::callbacks( (string) $hook ); } foreach ( (array) $events as $event ) { $out[] = CronEvent::fromArray( array( 'hook' => (string) $hook, 'timestamp' => (int) $timestamp, 'args' => serialize( isset( $event['args'] ) ? $event['args'] : array() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- the domain reads the stored form of the arguments. 'callbacks' => $cache[ $hook ], ) ); } } } return $out; } /** * Functions attached to a hook, with the file that defines them (Reflection). * * @param string $hook Hook name. * @return array */ private static function callbacks( $hook ) { global $wp_filter; if ( ! isset( $wp_filter[ $hook ] ) || ! is_object( $wp_filter[ $hook ] ) ) { return array(); } $out = array(); foreach ( (array) $wp_filter[ $hook ]->callbacks as $by_priority ) { foreach ( (array) $by_priority as $cb ) { $out[] = self::describe( $cb['function'] ); } } return $out; } /** * Name and defining file of one callback. * * @param mixed $callback Callback. * @return array{function:string, file:?string, inside:bool} */ private static function describe( $callback ) { $name = 'unknown'; $ref = null; try { if ( is_string( $callback ) ) { $name = $callback; if ( false !== strpos( $callback, '::' ) ) { list( $class, $method ) = explode( '::', $callback, 2 ); $ref = new \ReflectionMethod( $class, $method ); } elseif ( function_exists( $callback ) ) { $ref = new \ReflectionFunction( $callback ); } } elseif ( $callback instanceof \Closure ) { $name = '{closure}'; $ref = new \ReflectionFunction( $callback ); } elseif ( is_array( $callback ) && 2 === count( $callback ) && isset( $callback[0], $callback[1] ) ) { $class = is_object( $callback[0] ) ? get_class( $callback[0] ) : (string) $callback[0]; $name = $class . '::' . (string) $callback[1]; $ref = new \ReflectionMethod( $callback[0], (string) $callback[1] ); } elseif ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { $name = get_class( $callback ) . '::__invoke'; $ref = new \ReflectionMethod( $callback, '__invoke' ); } } catch ( \ReflectionException $e ) { $ref = null; } $file = $ref ? $ref->getFileName() : false; if ( ! is_string( $file ) || '' === $file ) { return array( 'function' => $name, 'file' => null, 'inside' => true, ); } $inside = Site::inside( $file ); return array( 'function' => $name, 'file' => $inside ? Site::relative( $file ) : wp_normalize_path( $file ), 'inside' => $inside, ); } /** * One page of autoloaded options plus widget_* and theme_mods_*. * * @param int $cursor Last option_id read. * @return array{items:OptionEntry[], cursor:int, done:bool} */ public static function options( $cursor ) { global $wpdb; $autoload = array_values( wp_autoload_values_to_autoload() ); $in = $autoload ? implode( ', ', array_fill( 0, count( $autoload ), '%s' ) ) : "''"; $sql = "SELECT option_id, option_name, SUBSTRING(option_value, 1, %d) AS option_value, autoload FROM {$wpdb->options} WHERE ( autoload IN ($in) OR option_name LIKE %s OR option_name LIKE %s ) AND option_id > %d ORDER BY option_id ASC LIMIT %d"; $values = array_merge( array( Assumptions::CONTENT_MAX_BYTES ), $autoload, array( $wpdb->esc_like( 'widget_' ) . '%', $wpdb->esc_like( 'theme_mods_' ) . '%', $cursor, self::OPTIONS_PAGE ) ); $rows = $wpdb->get_results( $wpdb->prepare( $sql, $values ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- placeholders built from counts only, values prepared. $items = array(); foreach ( (array) $rows as $row ) { $cursor = (int) $row['option_id']; $items[] = OptionEntry::fromArray( array( 'name' => (string) $row['option_name'], 'value' => (string) $row['option_value'], 'autoload' => (string) $row['autoload'], ) ); } return array( 'items' => $items, 'cursor' => $cursor, 'done' => count( (array) $rows ) < self::OPTIONS_PAGE, ); } /** Settings of control 9; home and siteurl are the raw stored values. */ public static function settings() { $active = array(); $list = maybe_unserialize( (string) self::raw_option( 'active_plugins' ) ); foreach ( is_array( $list ) ? $list : array() as $entry ) { if ( is_string( $entry ) ) { $active[] = array( 'entry' => $entry, 'option' => 'active_plugins', 'exists' => self::plugin_file_exists( $entry ), ); } } if ( is_multisite() ) { $network = get_site_option( 'active_sitewide_plugins', array() ); foreach ( is_array( $network ) ? array_keys( $network ) : array() as $entry ) { $active[] = array( 'entry' => (string) $entry, 'option' => 'active_sitewide_plugins', 'exists' => self::plugin_file_exists( (string) $entry ), ); } } return SettingsSnapshot::fromArray( array( 'siteurl' => (string) self::raw_option( 'siteurl' ), 'home' => (string) self::raw_option( 'home' ), 'wpHome' => defined( 'WP_HOME' ) ? (string) WP_HOME : null, 'wpSiteurl' => defined( 'WP_SITEURL' ) ? (string) WP_SITEURL : null, 'usersCanRegister' => (bool) get_option( 'users_can_register' ), 'defaultRole' => (string) get_option( 'default_role' ), 'activePlugins' => $active, ) ); } /** * Value stored in the options table, without filters nor constants. * * @param string $name Option name. */ private static function raw_option( $name ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the raw value is the point (the getter returns WP_HOME / WP_SITEURL). } /** * Whether an active_plugins entry names an existing file under the plugins folder. * * @param string $entry Entry of active_plugins. */ private static function plugin_file_exists( $entry ) { if ( '' === $entry || 0 !== validate_file( $entry ) ) { return false; } return is_file( WP_PLUGIN_DIR . '/' . $entry ); } }