class-wp-redis-user-session-storage.php 6.17 KB
Newer Older
1
<?php
Erick Hitter's avatar
Erick Hitter committed
2 3 4 5 6
/**
 * Offload session storage to Redis.
 *
 * @package WP_Redis_User_Session_Storage
 */
7

8 9 10 11 12 13 14
/**
 * Don't load in contexts that lack the WP_Session_Tokens class
 */
if ( ! class_exists( 'WP_Session_Tokens' ) ) {
	return;
}

15
/**
16
 * Redis-based user sessions token manager.
17
 *
18
 * @since 0.1
19
 */
20 21 22 23
class WP_Redis_User_Session_Storage extends WP_Session_Tokens {
	/**
	 * Holds the Redis client.
	 *
Erick Hitter's avatar
Erick Hitter committed
24
	 * @var Redis
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
	 */
	private $redis;

	/**
	 * Track if Redis is available
	 *
	 * @var bool
	 */
	private $redis_connected = false;

	/**
	 * Prefix used to namespace keys
	 *
	 * @var string
	 */
	public $prefix = 'wpruss';

	/**
43
	 * Create Redis connection using the Redis PECL extension
Erick Hitter's avatar
Erick Hitter committed
44 45
	 *
	 * @param int $user_id User ID.
46 47
	 */
	public function __construct( $user_id ) {
Erick Hitter's avatar
Erick Hitter committed
48
		// General Redis settings.
49
		$redis = array(
50 51
			'host'       => '127.0.0.1',
			'port'       => 6379,
52
			'socket'     => null,
53
			'serializer' => Redis::SERIALIZER_PHP,
54 55
		);

56 57
		if ( defined( 'WP_REDIS_USER_SESSION_HOST' ) && WP_REDIS_USER_SESSION_HOST ) {
			$redis['host'] = WP_REDIS_USER_SESSION_HOST;
58
		}
59 60
		if ( defined( 'WP_REDIS_USER_SESSION_PORT' ) && WP_REDIS_USER_SESSION_PORT ) {
			$redis['port'] = WP_REDIS_USER_SESSION_PORT;
61
		}
62 63 64
		if ( defined( 'WP_REDIS_USER_SESSION_SOCKET' ) && WP_REDIS_USER_SESSION_SOCKET ) {
			$redis['socket'] = WP_REDIS_USER_SESSION_SOCKET;
		}
65 66
		if ( defined( 'WP_REDIS_USER_SESSION_AUTH' ) && WP_REDIS_USER_SESSION_AUTH ) {
			$redis['auth'] = WP_REDIS_USER_SESSION_AUTH;
67
		}
68 69
		if ( defined( 'WP_REDIS_USER_SESSION_DB' ) && WP_REDIS_USER_SESSION_DB ) {
			$redis['database'] = WP_REDIS_USER_SESSION_DB;
70
		}
71
		if ( defined( 'WP_REDIS_USER_SESSION_SERIALIZER' ) && WP_REDIS_USER_SESSION_SERIALIZER ) {
Erick Hitter's avatar
Erick Hitter committed
72
			$redis['serializer'] = WP_REDIS_USER_SESSION_SERIALIZER;
73 74 75 76 77
		}

		// Use Redis PECL library.
		try {
			$this->redis = new Redis();
78

Erick Hitter's avatar
Erick Hitter committed
79
			// Socket preferred, but TCP supported.
80 81 82 83 84 85
			if ( $redis['socket'] ) {
				$this->redis->connect( $redis['socket'] );
			} else {
				$this->redis->connect( $redis['host'], $redis['port'] );
			}

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
			$this->redis->setOption( Redis::OPT_SERIALIZER, $redis['serializer'] );

			if ( isset( $redis['auth'] ) ) {
				$this->redis->auth( $redis['auth'] );
			}

			if ( isset( $redis['database'] ) ) {
				$this->redis->select( $redis['database'] );
			}

			$this->redis_connected = true;
		} catch ( RedisException $e ) {
			$this->redis_connected = false;
		}

Erick Hitter's avatar
Erick Hitter committed
101
		// Pass user ID to parent.
102 103
		parent::__construct( $user_id );
	}
104 105 106 107

	/**
	 * Get all sessions of a user.
	 *
108
	 * @since 0.1
109 110 111 112 113
	 * @access protected
	 *
	 * @return array Sessions of a user.
	 */
	protected function get_sessions() {
114 115 116 117 118 119 120 121 122
		if ( ! $this->redis_connected ) {
			return array();
		}

		$key = $this->get_key();

		if ( ! $this->redis->exists( $key ) ) {
			return array();
		}
123

124
		$sessions = $this->redis->get( $key );
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
		if ( ! is_array( $sessions ) ) {
			return array();
		}

		$sessions = array_map( array( $this, 'prepare_session' ), $sessions );
		return array_filter( $sessions, array( $this, 'is_still_valid' ) );
	}

	/**
	 * Converts an expiration to an array of session information.
	 *
	 * @param mixed $session Session or expiration.
	 * @return array Session.
	 */
	protected function prepare_session( $session ) {
		if ( is_int( $session ) ) {
			return array( 'expiration' => $session );
		}

		return $session;
	}

	/**
	 * Retrieve a session by its verifier (token hash).
	 *
150
	 * @since 0.1
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
	 * @access protected
	 *
	 * @param string $verifier Verifier of the session to retrieve.
	 * @return array|null The session, or null if it does not exist
	 */
	protected function get_session( $verifier ) {
		$sessions = $this->get_sessions();

		if ( isset( $sessions[ $verifier ] ) ) {
			return $sessions[ $verifier ];
		}

		return null;
	}

	/**
	 * Update a session by its verifier.
	 *
169
	 * @since 0.1
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
	 * @access protected
	 *
	 * @param string $verifier Verifier of the session to update.
	 * @param array  $session  Optional. Session. Omitting this argument destroys the session.
	 */
	protected function update_session( $verifier, $session = null ) {
		$sessions = $this->get_sessions();

		if ( $session ) {
			$sessions[ $verifier ] = $session;
		} else {
			unset( $sessions[ $verifier ] );
		}

		$this->update_sessions( $sessions );
	}

	/**
188
	 * Update a user's sessions in Redis.
189
	 *
190
	 * @since 0.1
191 192 193 194 195
	 * @access protected
	 *
	 * @param array $sessions Sessions.
	 */
	protected function update_sessions( $sessions ) {
196 197 198 199
		if ( ! $this->redis_connected ) {
			return;
		}

200 201 202 203
		if ( ! has_filter( 'attach_session_information' ) ) {
			$sessions = wp_list_pluck( $sessions, 'expiration' );
		}

204
		$key = $this->get_key();
205

206
		if ( $sessions ) {
207
			$this->redis->set( $key, $sessions );
208 209
		} elseif ( $this->redis->exists( $key ) ) {
			$this->redis->del( $key );
210 211 212 213 214 215
		}
	}

	/**
	 * Destroy all session tokens for a user, except a single session passed.
	 *
216
	 * @since 0.1
217 218 219 220 221 222 223 224 225 226 227 228
	 * @access protected
	 *
	 * @param string $verifier Verifier of the session to keep.
	 */
	protected function destroy_other_sessions( $verifier ) {
		$session = $this->get_session( $verifier );
		$this->update_sessions( array( $verifier => $session ) );
	}

	/**
	 * Destroy all session tokens for a user.
	 *
229
	 * @since 0.1
230 231 232 233 234 235 236 237 238
	 * @access protected
	 */
	protected function destroy_all_sessions() {
		$this->update_sessions( array() );
	}

	/**
	 * Destroy all session tokens for all users.
	 *
239
	 * @since 0.1
240 241
	 * @access public
	 * @static
Erick Hitter's avatar
Erick Hitter committed
242
	 *
243
	 * @return bool
244 245
	 */
	public static function drop_sessions() {
246 247 248 249 250 251
		return static::get_instance( 0 )->flush_redis_db();
	}

	/**
	 * Empty database, clearing all tokens.
	 *
Erick Hitter's avatar
Erick Hitter committed
252 253 254
	 * @since 0.2
	 * @access protected
	 *
255 256 257 258
	 * @return bool
	 */
	protected function flush_redis_db() {
		return $this->redis->flushDB();
259 260 261
	}

	/**
262
	 * Build key for current user
263
	 *
264 265 266 267
	 * @since 0.1
	 * @access protected
	 *
	 * @return string
268 269 270
	 */
	protected function get_key() {
		return $this->prefix . ':' . $this->user_id;
271
	}
Erick Hitter's avatar
Erick Hitter committed
272 273 274 275 276 277 278 279 280

	/**
	 * Is Redis connected?
	 *
	 * @return bool
	 */
	public function redis_connected() {
		return $this->redis_connected;
	}
281
}
282 283

/**
284
 * Override Core's default usermeta-based token storage
285 286
 *
 * @return string
287
 */
288
function wp_redis_user_session_storage() {
289
	return 'WP_Redis_User_Session_Storage';
290 291
}
add_filter( 'session_token_manager', 'wp_redis_user_session_storage' );