Completed
Branch master (939199)
by
unknown
39:35
created

includes/libs/objectcache/MemcachedClient.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
// @codingStandardsIgnoreFile It's an external lib and it isn't. Let's not bother.
3
/**
4
 * Memcached client for PHP.
5
 *
6
 * +---------------------------------------------------------------------------+
7
 * | memcached client, PHP                                                     |
8
 * +---------------------------------------------------------------------------+
9
 * | Copyright (c) 2003 Ryan T. Dean <[email protected]>                 |
10
 * | All rights reserved.                                                      |
11
 * |                                                                           |
12
 * | Redistribution and use in source and binary forms, with or without        |
13
 * | modification, are permitted provided that the following conditions        |
14
 * | are met:                                                                  |
15
 * |                                                                           |
16
 * | 1. Redistributions of source code must retain the above copyright         |
17
 * |    notice, this list of conditions and the following disclaimer.          |
18
 * | 2. Redistributions in binary form must reproduce the above copyright      |
19
 * |    notice, this list of conditions and the following disclaimer in the    |
20
 * |    documentation and/or other materials provided with the distribution.   |
21
 * |                                                                           |
22
 * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
23
 * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
24
 * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
25
 * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
26
 * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
27
 * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
28
 * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
29
 * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
30
 * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
31
 * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
32
 * +---------------------------------------------------------------------------+
33
 * | Author: Ryan T. Dean <[email protected]>                            |
34
 * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
35
 * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
36
 * |   client logic under 2-clause BSD license.                                |
37
 * +---------------------------------------------------------------------------+
38
 *
39
 * @file
40
 * $TCAnet$
41
 */
42
43
/**
44
 * This is a PHP client for memcached - a distributed memory cache daemon.
45
 *
46
 * More information is available at http://www.danga.com/memcached/
47
 *
48
 * Usage example:
49
 *
50
 *     $mc = new MemcachedClient(array(
51
 *         'servers' => array(
52
 *             '127.0.0.1:10000',
53
 *             array( '192.0.0.1:10010', 2 ),
54
 *             '127.0.0.1:10020'
55
 *         ),
56
 *         'debug'   => false,
57
 *         'compress_threshold' => 10240,
58
 *         'persistent' => true
59
 *     ));
60
 *
61
 *     $mc->add( 'key', array( 'some', 'array' ) );
62
 *     $mc->replace( 'key', 'some random string' );
63
 *     $val = $mc->get( 'key' );
64
 *
65
 * @author Ryan T. Dean <[email protected]>
66
 * @version 0.1.2
67
 */
68
69
use Psr\Log\LoggerInterface;
70
use Psr\Log\NullLogger;
71
72
// {{{ class MemcachedClient
73
/**
74
 * memcached client class implemented using (p)fsockopen()
75
 *
76
 * @author  Ryan T. Dean <[email protected]>
77
 * @ingroup Cache
78
 */
79
class MemcachedClient {
80
	// {{{ properties
81
	// {{{ public
82
83
	// {{{ constants
84
	// {{{ flags
85
86
	/**
87
	 * Flag: indicates data is serialized
88
	 */
89
	const SERIALIZED = 1;
90
91
	/**
92
	 * Flag: indicates data is compressed
93
	 */
94
	const COMPRESSED = 2;
95
96
	/**
97
	 * Flag: indicates data is an integer
98
	 */
99
	const INTVAL = 4;
100
101
	// }}}
102
103
	/**
104
	 * Minimum savings to store data compressed
105
	 */
106
	const COMPRESSION_SAVINGS = 0.20;
107
108
	// }}}
109
110
	/**
111
	 * Command statistics
112
	 *
113
	 * @var array
114
	 * @access public
115
	 */
116
	public $stats;
117
118
	// }}}
119
	// {{{ private
120
121
	/**
122
	 * Cached Sockets that are connected
123
	 *
124
	 * @var array
125
	 * @access private
126
	 */
127
	public $_cache_sock;
128
129
	/**
130
	 * Current debug status; 0 - none to 9 - profiling
131
	 *
132
	 * @var bool
133
	 * @access private
134
	 */
135
	public $_debug;
136
137
	/**
138
	 * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
139
	 *
140
	 * @var array
141
	 * @access private
142
	 */
143
	public $_host_dead;
144
145
	/**
146
	 * Is compression available?
147
	 *
148
	 * @var bool
149
	 * @access private
150
	 */
151
	public $_have_zlib;
152
153
	/**
154
	 * Do we want to use compression?
155
	 *
156
	 * @var bool
157
	 * @access private
158
	 */
159
	public $_compress_enable;
160
161
	/**
162
	 * At how many bytes should we compress?
163
	 *
164
	 * @var int
165
	 * @access private
166
	 */
167
	public $_compress_threshold;
168
169
	/**
170
	 * Are we using persistent links?
171
	 *
172
	 * @var bool
173
	 * @access private
174
	 */
175
	public $_persistent;
176
177
	/**
178
	 * If only using one server; contains ip:port to connect to
179
	 *
180
	 * @var string
181
	 * @access private
182
	 */
183
	public $_single_sock;
184
185
	/**
186
	 * Array containing ip:port or array(ip:port, weight)
187
	 *
188
	 * @var array
189
	 * @access private
190
	 */
191
	public $_servers;
192
193
	/**
194
	 * Our bit buckets
195
	 *
196
	 * @var array
197
	 * @access private
198
	 */
199
	public $_buckets;
200
201
	/**
202
	 * Total # of bit buckets we have
203
	 *
204
	 * @var int
205
	 * @access private
206
	 */
207
	public $_bucketcount;
208
209
	/**
210
	 * # of total servers we have
211
	 *
212
	 * @var int
213
	 * @access private
214
	 */
215
	public $_active;
216
217
	/**
218
	 * Stream timeout in seconds. Applies for example to fread()
219
	 *
220
	 * @var int
221
	 * @access private
222
	 */
223
	public $_timeout_seconds;
224
225
	/**
226
	 * Stream timeout in microseconds
227
	 *
228
	 * @var int
229
	 * @access private
230
	 */
231
	public $_timeout_microseconds;
232
233
	/**
234
	 * Connect timeout in seconds
235
	 */
236
	public $_connect_timeout;
237
238
	/**
239
	 * Number of connection attempts for each server
240
	 */
241
	public $_connect_attempts;
242
243
	/**
244
	 * @var LoggerInterface
245
	 */
246
	private $_logger;
247
248
	// }}}
249
	// }}}
250
	// {{{ methods
251
	// {{{ public functions
252
	// {{{ memcached()
253
254
	/**
255
	 * Memcache initializer
256
	 *
257
	 * @param array $args Associative array of settings
258
	 *
259
	 * @return mixed
260
	 */
261
	public function __construct( $args ) {
262
		$this->set_servers( isset( $args['servers'] ) ? $args['servers'] : array() );
263
		$this->_debug = isset( $args['debug'] ) ? $args['debug'] : false;
264
		$this->stats = array();
265
		$this->_compress_threshold = isset( $args['compress_threshold'] ) ? $args['compress_threshold'] : 0;
266
		$this->_persistent = isset( $args['persistent'] ) ? $args['persistent'] : false;
267
		$this->_compress_enable = true;
268
		$this->_have_zlib = function_exists( 'gzcompress' );
269
270
		$this->_cache_sock = array();
271
		$this->_host_dead = array();
272
273
		$this->_timeout_seconds = 0;
274
		$this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 500000;
275
276
		$this->_connect_timeout = isset( $args['connect_timeout'] ) ? $args['connect_timeout'] : 0.1;
277
		$this->_connect_attempts = 2;
278
279
		$this->_logger = isset( $args['logger'] ) ? $args['logger'] : new NullLogger();
280
	}
281
282
	// }}}
283
	// {{{ add()
284
285
	/**
286
	 * Adds a key/value to the memcache server if one isn't already set with
287
	 * that key
288
	 *
289
	 * @param string $key Key to set with data
290
	 * @param mixed $val Value to store
291
	 * @param int $exp (optional) Expiration time. This can be a number of seconds
292
	 * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
293
	 * longer must be the timestamp of the time at which the mapping should expire. It
294
	 * is safe to use timestamps in all cases, regardless of expiration
295
	 * eg: strtotime("+3 hour")
296
	 *
297
	 * @return bool
298
	 */
299
	public function add( $key, $val, $exp = 0 ) {
300
		return $this->_set( 'add', $key, $val, $exp );
301
	}
302
303
	// }}}
304
	// {{{ decr()
305
306
	/**
307
	 * Decrease a value stored on the memcache server
308
	 *
309
	 * @param string $key Key to decrease
310
	 * @param int $amt (optional) amount to decrease
311
	 *
312
	 * @return mixed False on failure, value on success
313
	 */
314
	public function decr( $key, $amt = 1 ) {
315
		return $this->_incrdecr( 'decr', $key, $amt );
316
	}
317
318
	// }}}
319
	// {{{ delete()
320
321
	/**
322
	 * Deletes a key from the server, optionally after $time
323
	 *
324
	 * @param string $key Key to delete
325
	 * @param int $time (optional) how long to wait before deleting
326
	 *
327
	 * @return bool True on success, false on failure
328
	 */
329 View Code Duplication
	public function delete( $key, $time = 0 ) {
330
		if ( !$this->_active ) {
331
			return false;
332
		}
333
334
		$sock = $this->get_sock( $key );
335
		if ( !is_resource( $sock ) ) {
336
			return false;
337
		}
338
339
		$key = is_array( $key ) ? $key[1] : $key;
340
341
		if ( isset( $this->stats['delete'] ) ) {
342
			$this->stats['delete']++;
343
		} else {
344
			$this->stats['delete'] = 1;
345
		}
346
		$cmd = "delete $key $time\r\n";
347
		if ( !$this->_fwrite( $sock, $cmd ) ) {
348
			return false;
349
		}
350
		$res = $this->_fgets( $sock );
351
352
		if ( $this->_debug ) {
353
			$this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
354
		}
355
356
		if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
357
			return true;
358
		}
359
360
		return false;
361
	}
362
363
	/**
364
	 * Changes the TTL on a key from the server to $time
365
	 *
366
	 * @param string $key Key
367
	 * @param int $time TTL in seconds
368
	 *
369
	 * @return bool True on success, false on failure
370
	 */
371 View Code Duplication
	public function touch( $key, $time = 0 ) {
372
		if ( !$this->_active ) {
373
			return false;
374
		}
375
376
		$sock = $this->get_sock( $key );
377
		if ( !is_resource( $sock ) ) {
378
			return false;
379
		}
380
381
		$key = is_array( $key ) ? $key[1] : $key;
382
383
		if ( isset( $this->stats['touch'] ) ) {
384
			$this->stats['touch']++;
385
		} else {
386
			$this->stats['touch'] = 1;
387
		}
388
		$cmd = "touch $key $time\r\n";
389
		if ( !$this->_fwrite( $sock, $cmd ) ) {
390
			return false;
391
		}
392
		$res = $this->_fgets( $sock );
393
394
		if ( $this->_debug ) {
395
			$this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
396
		}
397
398
		if ( $res == "TOUCHED" ) {
399
			return true;
400
		}
401
402
		return false;
403
	}
404
405
	/**
406
	 * @param string $key
407
	 * @param int $timeout
408
	 * @return bool
409
	 */
410
	public function lock( $key, $timeout = 0 ) {
411
		/* stub */
412
		return true;
413
	}
414
415
	/**
416
	 * @param string $key
417
	 * @return bool
418
	 */
419
	public function unlock( $key ) {
420
		/* stub */
421
		return true;
422
	}
423
424
	// }}}
425
	// {{{ disconnect_all()
426
427
	/**
428
	 * Disconnects all connected sockets
429
	 */
430
	public function disconnect_all() {
431
		foreach ( $this->_cache_sock as $sock ) {
432
			fclose( $sock );
433
		}
434
435
		$this->_cache_sock = array();
436
	}
437
438
	// }}}
439
	// {{{ enable_compress()
440
441
	/**
442
	 * Enable / Disable compression
443
	 *
444
	 * @param bool $enable True to enable, false to disable
445
	 */
446
	public function enable_compress( $enable ) {
447
		$this->_compress_enable = $enable;
448
	}
449
450
	// }}}
451
	// {{{ forget_dead_hosts()
452
453
	/**
454
	 * Forget about all of the dead hosts
455
	 */
456
	public function forget_dead_hosts() {
457
		$this->_host_dead = array();
458
	}
459
460
	// }}}
461
	// {{{ get()
462
463
	/**
464
	 * Retrieves the value associated with the key from the memcache server
465
	 *
466
	 * @param array|string $key key to retrieve
467
	 * @param float $casToken [optional]
468
	 *
469
	 * @return mixed
470
	 */
471
	public function get( $key, &$casToken = null ) {
472
473
		if ( $this->_debug ) {
474
			$this->_debugprint( "get($key)" );
475
		}
476
477
		if ( !is_array( $key ) && strval( $key ) === '' ) {
478
			$this->_debugprint( "Skipping key which equals to an empty string" );
479
			return false;
480
		}
481
482
		if ( !$this->_active ) {
483
			return false;
484
		}
485
486
		$sock = $this->get_sock( $key );
0 ignored issues
show
It seems like $key defined by parameter $key on line 471 can also be of type array; however, MemcachedClient::get_sock() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
487
488
		if ( !is_resource( $sock ) ) {
489
			return false;
490
		}
491
492
		$key = is_array( $key ) ? $key[1] : $key;
493
		if ( isset( $this->stats['get'] ) ) {
494
			$this->stats['get']++;
495
		} else {
496
			$this->stats['get'] = 1;
497
		}
498
499
		$cmd = "gets $key\r\n";
500
		if ( !$this->_fwrite( $sock, $cmd ) ) {
501
			return false;
502
		}
503
504
		$val = array();
505
		$this->_load_items( $sock, $val, $casToken );
506
507
		if ( $this->_debug ) {
508
			foreach ( $val as $k => $v ) {
509
				$this->_debugprint( sprintf( "MemCache: sock %s got %s", serialize( $sock ), $k ) );
510
			}
511
		}
512
513
		$value = false;
514
		if ( isset( $val[$key] ) ) {
515
			$value = $val[$key];
516
		}
517
		return $value;
518
	}
519
520
	// }}}
521
	// {{{ get_multi()
522
523
	/**
524
	 * Get multiple keys from the server(s)
525
	 *
526
	 * @param array $keys Keys to retrieve
527
	 *
528
	 * @return array
529
	 */
530
	public function get_multi( $keys ) {
531
		if ( !$this->_active ) {
532
			return array();
533
		}
534
535
		if ( isset( $this->stats['get_multi'] ) ) {
536
			$this->stats['get_multi']++;
537
		} else {
538
			$this->stats['get_multi'] = 1;
539
		}
540
		$sock_keys = array();
541
		$socks = array();
542
		foreach ( $keys as $key ) {
543
			$sock = $this->get_sock( $key );
544
			if ( !is_resource( $sock ) ) {
545
				continue;
546
			}
547
			$key = is_array( $key ) ? $key[1] : $key;
548
			if ( !isset( $sock_keys[$sock] ) ) {
549
				$sock_keys[intval( $sock )] = array();
550
				$socks[] = $sock;
551
			}
552
			$sock_keys[intval( $sock )][] = $key;
553
		}
554
555
		$gather = array();
556
		// Send out the requests
557
		foreach ( $socks as $sock ) {
558
			$cmd = 'gets';
559
			foreach ( $sock_keys[intval( $sock )] as $key ) {
560
				$cmd .= ' ' . $key;
561
			}
562
			$cmd .= "\r\n";
563
564
			if ( $this->_fwrite( $sock, $cmd ) ) {
565
				$gather[] = $sock;
566
			}
567
		}
568
569
		// Parse responses
570
		$val = array();
571
		foreach ( $gather as $sock ) {
572
			$this->_load_items( $sock, $val, $casToken );
573
		}
574
575
		if ( $this->_debug ) {
576
			foreach ( $val as $k => $v ) {
577
				$this->_debugprint( sprintf( "MemCache: got %s", $k ) );
578
			}
579
		}
580
581
		return $val;
582
	}
583
584
	// }}}
585
	// {{{ incr()
586
587
	/**
588
	 * Increments $key (optionally) by $amt
589
	 *
590
	 * @param string $key Key to increment
591
	 * @param int $amt (optional) amount to increment
592
	 *
593
	 * @return int|null Null if the key does not exist yet (this does NOT
594
	 * create new mappings if the key does not exist). If the key does
595
	 * exist, this returns the new value for that key.
596
	 */
597
	public function incr( $key, $amt = 1 ) {
598
		return $this->_incrdecr( 'incr', $key, $amt );
599
	}
600
601
	// }}}
602
	// {{{ replace()
603
604
	/**
605
	 * Overwrites an existing value for key; only works if key is already set
606
	 *
607
	 * @param string $key Key to set value as
608
	 * @param mixed $value Value to store
609
	 * @param int $exp (optional) Expiration time. This can be a number of seconds
610
	 * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
611
	 * longer must be the timestamp of the time at which the mapping should expire. It
612
	 * is safe to use timestamps in all cases, regardless of exipration
613
	 * eg: strtotime("+3 hour")
614
	 *
615
	 * @return bool
616
	 */
617
	public function replace( $key, $value, $exp = 0 ) {
618
		return $this->_set( 'replace', $key, $value, $exp );
619
	}
620
621
	// }}}
622
	// {{{ run_command()
623
624
	/**
625
	 * Passes through $cmd to the memcache server connected by $sock; returns
626
	 * output as an array (null array if no output)
627
	 *
628
	 * @param Resource $sock Socket to send command on
629
	 * @param string $cmd Command to run
630
	 *
631
	 * @return array Output array
632
	 */
633
	public function run_command( $sock, $cmd ) {
634
		if ( !is_resource( $sock ) ) {
635
			return array();
636
		}
637
638
		if ( !$this->_fwrite( $sock, $cmd ) ) {
639
			return array();
640
		}
641
642
		$ret = array();
643
		while ( true ) {
644
			$res = $this->_fgets( $sock );
645
			$ret[] = $res;
646
			if ( preg_match( '/^END/', $res ) ) {
647
				break;
648
			}
649
			if ( strlen( $res ) == 0 ) {
650
				break;
651
			}
652
		}
653
		return $ret;
654
	}
655
656
	// }}}
657
	// {{{ set()
658
659
	/**
660
	 * Unconditionally sets a key to a given value in the memcache.  Returns true
661
	 * if set successfully.
662
	 *
663
	 * @param string $key Key to set value as
664
	 * @param mixed $value Value to set
665
	 * @param int $exp (optional) Expiration time. This can be a number of seconds
666
	 * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
667
	 * longer must be the timestamp of the time at which the mapping should expire. It
668
	 * is safe to use timestamps in all cases, regardless of exipration
669
	 * eg: strtotime("+3 hour")
670
	 *
671
	 * @return bool True on success
672
	 */
673
	public function set( $key, $value, $exp = 0 ) {
674
		return $this->_set( 'set', $key, $value, $exp );
675
	}
676
677
	// }}}
678
	// {{{ cas()
679
680
	/**
681
	 * Sets a key to a given value in the memcache if the current value still corresponds
682
	 * to a known, given value.  Returns true if set successfully.
683
	 *
684
	 * @param float $casToken Current known value
685
	 * @param string $key Key to set value as
686
	 * @param mixed $value Value to set
687
	 * @param int $exp (optional) Expiration time. This can be a number of seconds
688
	 * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
689
	 * longer must be the timestamp of the time at which the mapping should expire. It
690
	 * is safe to use timestamps in all cases, regardless of exipration
691
	 * eg: strtotime("+3 hour")
692
	 *
693
	 * @return bool True on success
694
	 */
695
	public function cas( $casToken, $key, $value, $exp = 0 ) {
696
		return $this->_set( 'cas', $key, $value, $exp, $casToken );
697
	}
698
699
	// }}}
700
	// {{{ set_compress_threshold()
701
702
	/**
703
	 * Set the compression threshold
704
	 *
705
	 * @param int $thresh Threshold to compress if larger than
706
	 */
707
	public function set_compress_threshold( $thresh ) {
708
		$this->_compress_threshold = $thresh;
709
	}
710
711
	// }}}
712
	// {{{ set_debug()
713
714
	/**
715
	 * Set the debug flag
716
	 *
717
	 * @see __construct()
718
	 * @param bool $dbg True for debugging, false otherwise
719
	 */
720
	public function set_debug( $dbg ) {
721
		$this->_debug = $dbg;
722
	}
723
724
	// }}}
725
	// {{{ set_servers()
726
727
	/**
728
	 * Set the server list to distribute key gets and puts between
729
	 *
730
	 * @see __construct()
731
	 * @param array $list Array of servers to connect to
732
	 */
733
	public function set_servers( $list ) {
734
		$this->_servers = $list;
735
		$this->_active = count( $list );
736
		$this->_buckets = null;
737
		$this->_bucketcount = 0;
738
739
		$this->_single_sock = null;
740
		if ( $this->_active == 1 ) {
741
			$this->_single_sock = $this->_servers[0];
742
		}
743
	}
744
745
	/**
746
	 * Sets the timeout for new connections
747
	 *
748
	 * @param int $seconds Number of seconds
749
	 * @param int $microseconds Number of microseconds
750
	 */
751
	public function set_timeout( $seconds, $microseconds ) {
752
		$this->_timeout_seconds = $seconds;
753
		$this->_timeout_microseconds = $microseconds;
754
	}
755
756
	// }}}
757
	// }}}
758
	// {{{ private methods
759
	// {{{ _close_sock()
760
761
	/**
762
	 * Close the specified socket
763
	 *
764
	 * @param string $sock Socket to close
765
	 *
766
	 * @access private
767
	 */
768
	function _close_sock( $sock ) {
769
		$host = array_search( $sock, $this->_cache_sock );
770
		fclose( $this->_cache_sock[$host] );
771
		unset( $this->_cache_sock[$host] );
772
	}
773
774
	// }}}
775
	// {{{ _connect_sock()
776
777
	/**
778
	 * Connects $sock to $host, timing out after $timeout
779
	 *
780
	 * @param int $sock Socket to connect
781
	 * @param string $host Host:IP to connect to
782
	 *
783
	 * @return bool
784
	 * @access private
785
	 */
786
	function _connect_sock( &$sock, $host ) {
787
		list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
788
		$sock = false;
789
		$timeout = $this->_connect_timeout;
790
		$errno = $errstr = null;
791
		for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
792
			MediaWiki\suppressWarnings();
793
			if ( $this->_persistent == 1 ) {
794
				$sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
795
			} else {
796
				$sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
797
			}
798
			MediaWiki\restoreWarnings();
799
		}
800
		if ( !$sock ) {
801
			$this->_error_log( "Error connecting to $host: $errstr" );
802
			$this->_dead_host( $host );
803
			return false;
804
		}
805
806
		// Initialise timeout
807
		stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
808
809
		// If the connection was persistent, flush the read buffer in case there
810
		// was a previous incomplete request on this connection
811
		if ( $this->_persistent ) {
812
			$this->_flush_read_buffer( $sock );
813
		}
814
		return true;
815
	}
816
817
	// }}}
818
	// {{{ _dead_sock()
819
820
	/**
821
	 * Marks a host as dead until 30-40 seconds in the future
822
	 *
823
	 * @param string $sock Socket to mark as dead
824
	 *
825
	 * @access private
826
	 */
827
	function _dead_sock( $sock ) {
828
		$host = array_search( $sock, $this->_cache_sock );
829
		$this->_dead_host( $host );
830
	}
831
832
	/**
833
	 * @param string $host
834
	 */
835
	function _dead_host( $host ) {
836
		$ip = explode( ':', $host )[0];
837
		$this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
838
		$this->_host_dead[$host] = $this->_host_dead[$ip];
839
		unset( $this->_cache_sock[$host] );
840
	}
841
842
	// }}}
843
	// {{{ get_sock()
844
845
	/**
846
	 * get_sock
847
	 *
848
	 * @param string $key Key to retrieve value for;
849
	 *
850
	 * @return Resource|bool Resource on success, false on failure
851
	 * @access private
852
	 */
853
	function get_sock( $key ) {
854
		if ( !$this->_active ) {
855
			return false;
856
		}
857
858
		if ( $this->_single_sock !== null ) {
859
			return $this->sock_to_host( $this->_single_sock );
860
		}
861
862
		$hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
863
		if ( $this->_buckets === null ) {
864
			$bu = array();
865
			foreach ( $this->_servers as $v ) {
866
				if ( is_array( $v ) ) {
867
					for ( $i = 0; $i < $v[1]; $i++ ) {
868
						$bu[] = $v[0];
869
					}
870
				} else {
871
					$bu[] = $v;
872
				}
873
			}
874
			$this->_buckets = $bu;
875
			$this->_bucketcount = count( $bu );
876
		}
877
878
		$realkey = is_array( $key ) ? $key[1] : $key;
879
		for ( $tries = 0; $tries < 20; $tries++ ) {
880
			$host = $this->_buckets[$hv % $this->_bucketcount];
881
			$sock = $this->sock_to_host( $host );
882
			if ( is_resource( $sock ) ) {
883
				return $sock;
884
			}
885
			$hv = $this->_hashfunc( $hv . $realkey );
886
		}
887
888
		return false;
889
	}
890
891
	// }}}
892
	// {{{ _hashfunc()
893
894
	/**
895
	 * Creates a hash integer based on the $key
896
	 *
897
	 * @param string $key Key to hash
898
	 *
899
	 * @return int Hash value
900
	 * @access private
901
	 */
902
	function _hashfunc( $key ) {
903
		# Hash function must be in [0,0x7ffffff]
904
		# We take the first 31 bits of the MD5 hash, which unlike the hash
905
		# function used in a previous version of this client, works
906
		return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
907
	}
908
909
	// }}}
910
	// {{{ _incrdecr()
911
912
	/**
913
	 * Perform increment/decriment on $key
914
	 *
915
	 * @param string $cmd Command to perform
916
	 * @param string|array $key Key to perform it on
917
	 * @param int $amt Amount to adjust
918
	 *
919
	 * @return int New value of $key
920
	 * @access private
921
	 */
922
	function _incrdecr( $cmd, $key, $amt = 1 ) {
923
		if ( !$this->_active ) {
924
			return null;
925
		}
926
927
		$sock = $this->get_sock( $key );
0 ignored issues
show
It seems like $key defined by parameter $key on line 922 can also be of type array; however, MemcachedClient::get_sock() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
928
		if ( !is_resource( $sock ) ) {
929
			return null;
930
		}
931
932
		$key = is_array( $key ) ? $key[1] : $key;
933 View Code Duplication
		if ( isset( $this->stats[$cmd] ) ) {
934
			$this->stats[$cmd]++;
935
		} else {
936
			$this->stats[$cmd] = 1;
937
		}
938
		if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
939
			return null;
940
		}
941
942
		$line = $this->_fgets( $sock );
943
		$match = array();
944
		if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
945
			return null;
946
		}
947
		return $match[1];
948
	}
949
950
	// }}}
951
	// {{{ _load_items()
952
953
	/**
954
	 * Load items into $ret from $sock
955
	 *
956
	 * @param Resource $sock Socket to read from
957
	 * @param array $ret returned values
958
	 * @param float $casToken [optional]
959
	 * @return bool True for success, false for failure
960
	 *
961
	 * @access private
962
	 */
963
	function _load_items( $sock, &$ret, &$casToken = null ) {
964
		$results = array();
965
966
		while ( 1 ) {
967
			$decl = $this->_fgets( $sock );
968
969
			if ( $decl === false ) {
970
				/*
971
				 * If nothing can be read, something is wrong because we know exactly when
972
				 * to stop reading (right after "END") and we return right after that.
973
				 */
974
				return false;
975
			} elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
976
				/*
977
				 * Read all data returned. This can be either one or multiple values.
978
				 * Save all that data (in an array) to be processed later: we'll first
979
				 * want to continue reading until "END" before doing anything else,
980
				 * to make sure that we don't leave our client in a state where it's
981
				 * output is not yet fully read.
982
				 */
983
				$results[] = array(
984
					$match[1], // rkey
985
					$match[2], // flags
986
					$match[3], // len
987
					$match[4], // casToken
988
					$this->_fread( $sock, $match[3] + 2 ), // data
989
				);
990
			} elseif ( $decl == "END" ) {
991
				if ( count( $results ) == 0 ) {
992
					return false;
993
				}
994
995
				/**
996
				 * All data has been read, time to process the data and build
997
				 * meaningful return values.
998
				 */
999
				foreach ( $results as $vars ) {
1000
					list( $rkey, $flags, $len, $casToken, $data ) = $vars;
1001
1002
					if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
1003
						$this->_handle_error( $sock,
1004
							'line ending missing from data block from $1' );
1005
						return false;
1006
					}
1007
					$data = substr( $data, 0, -2 );
1008
					$ret[$rkey] = $data;
1009
1010
					if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
1011
						$ret[$rkey] = gzuncompress( $ret[$rkey] );
1012
					}
1013
1014
					/*
1015
					 * This unserialize is the exact reason that we only want to
1016
					 * process data after having read until "END" (instead of doing
1017
					 * this right away): "unserialize" can trigger outside code:
1018
					 * in the event that $ret[$rkey] is a serialized object,
1019
					 * unserializing it will trigger __wakeup() if present. If that
1020
					 * function attempted to read from memcached (while we did not
1021
					 * yet read "END"), these 2 calls would collide.
1022
					 */
1023
					if ( $flags & self::SERIALIZED ) {
1024
						$ret[$rkey] = unserialize( $ret[$rkey] );
1025
					} elseif ( $flags & self::INTVAL ) {
1026
						$ret[$rkey] = intval( $ret[$rkey] );
1027
					}
1028
				}
1029
1030
				return true;
1031
			} else {
1032
				$this->_handle_error( $sock, 'Error parsing response from $1' );
1033
				return false;
1034
			}
1035
		}
1036
	}
1037
1038
	// }}}
1039
	// {{{ _set()
1040
1041
	/**
1042
	 * Performs the requested storage operation to the memcache server
1043
	 *
1044
	 * @param string $cmd Command to perform
1045
	 * @param string $key Key to act on
1046
	 * @param mixed $val What we need to store
1047
	 * @param int $exp (optional) Expiration time. This can be a number of seconds
1048
	 * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
1049
	 * longer must be the timestamp of the time at which the mapping should expire. It
1050
	 * is safe to use timestamps in all cases, regardless of exipration
1051
	 * eg: strtotime("+3 hour")
1052
	 * @param float $casToken [optional]
1053
	 *
1054
	 * @return bool
1055
	 * @access private
1056
	 */
1057
	function _set( $cmd, $key, $val, $exp, $casToken = null ) {
1058
		if ( !$this->_active ) {
1059
			return false;
1060
		}
1061
1062
		$sock = $this->get_sock( $key );
1063
		if ( !is_resource( $sock ) ) {
1064
			return false;
1065
		}
1066
1067 View Code Duplication
		if ( isset( $this->stats[$cmd] ) ) {
1068
			$this->stats[$cmd]++;
1069
		} else {
1070
			$this->stats[$cmd] = 1;
1071
		}
1072
1073
		$flags = 0;
1074
1075
		if ( is_int( $val ) ) {
1076
			$flags |= self::INTVAL;
1077
		} elseif ( !is_scalar( $val ) ) {
1078
			$val = serialize( $val );
1079
			$flags |= self::SERIALIZED;
1080
			if ( $this->_debug ) {
1081
				$this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
1082
			}
1083
		}
1084
1085
		$len = strlen( $val );
1086
1087
		if ( $this->_have_zlib && $this->_compress_enable
1088
			&& $this->_compress_threshold && $len >= $this->_compress_threshold
1089
		) {
1090
			$c_val = gzcompress( $val, 9 );
1091
			$c_len = strlen( $c_val );
1092
1093
			if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
1094
				if ( $this->_debug ) {
1095
					$this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
1096
				}
1097
				$val = $c_val;
1098
				$len = $c_len;
1099
				$flags |= self::COMPRESSED;
1100
			}
1101
		}
1102
1103
		$command = "$cmd $key $flags $exp $len";
1104
		if ( $casToken ) {
1105
			$command .= " $casToken";
1106
		}
1107
1108
		if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
1109
			return false;
1110
		}
1111
1112
		$line = $this->_fgets( $sock );
1113
1114
		if ( $this->_debug ) {
1115
			$this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
1116
		}
1117
		if ( $line == "STORED" ) {
1118
			return true;
1119
		}
1120
		return false;
1121
	}
1122
1123
	// }}}
1124
	// {{{ sock_to_host()
1125
1126
	/**
1127
	 * Returns the socket for the host
1128
	 *
1129
	 * @param string $host Host:IP to get socket for
1130
	 *
1131
	 * @return Resource|bool IO Stream or false
1132
	 * @access private
1133
	 */
1134
	function sock_to_host( $host ) {
1135
		if ( isset( $this->_cache_sock[$host] ) ) {
1136
			return $this->_cache_sock[$host];
1137
		}
1138
1139
		$sock = null;
1140
		$now = time();
1141
		list( $ip, /* $port */) = explode( ':', $host );
1142
		if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
1143
			isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
1144
		) {
1145
			return null;
1146
		}
1147
1148
		if ( !$this->_connect_sock( $sock, $host ) ) {
1149
			return null;
1150
		}
1151
1152
		// Do not buffer writes
1153
		stream_set_write_buffer( $sock, 0 );
1154
1155
		$this->_cache_sock[$host] = $sock;
1156
1157
		return $this->_cache_sock[$host];
1158
	}
1159
1160
	/**
1161
	 * @param string $text
1162
	 */
1163
	function _debugprint( $text ) {
1164
		$this->_logger->debug( $text );
1165
	}
1166
1167
	/**
1168
	 * @param string $text
1169
	 */
1170
	function _error_log( $text ) {
1171
		$this->_logger->error( "Memcached error: $text" );
1172
	}
1173
1174
	/**
1175
	 * Write to a stream. If there is an error, mark the socket dead.
1176
	 *
1177
	 * @param Resource $sock The socket
1178
	 * @param string $buf The string to write
1179
	 * @return bool True on success, false on failure
1180
	 */
1181
	function _fwrite( $sock, $buf ) {
1182
		$bytesWritten = 0;
1183
		$bufSize = strlen( $buf );
1184
		while ( $bytesWritten < $bufSize ) {
1185
			$result = fwrite( $sock, $buf );
1186
			$data = stream_get_meta_data( $sock );
1187
			if ( $data['timed_out'] ) {
1188
				$this->_handle_error( $sock, 'timeout writing to $1' );
1189
				return false;
1190
			}
1191
			// Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
1192
			if ( $result === false || $result === 0 ) {
1193
				$this->_handle_error( $sock, 'error writing to $1' );
1194
				return false;
1195
			}
1196
			$bytesWritten += $result;
1197
		}
1198
1199
		return true;
1200
	}
1201
1202
	/**
1203
	 * Handle an I/O error. Mark the socket dead and log an error.
1204
	 *
1205
	 * @param Resource $sock
1206
	 * @param string $msg
1207
	 */
1208
	function _handle_error( $sock, $msg ) {
1209
		$peer = stream_socket_get_name( $sock, true /** remote **/ );
1210
		if ( strval( $peer ) === '' ) {
1211
			$peer = array_search( $sock, $this->_cache_sock );
1212
			if ( $peer === false ) {
1213
				$peer = '[unknown host]';
1214
			}
1215
		}
1216
		$msg = str_replace( '$1', $peer, $msg );
1217
		$this->_error_log( "$msg" );
1218
		$this->_dead_sock( $sock );
1219
	}
1220
1221
	/**
1222
	 * Read the specified number of bytes from a stream. If there is an error,
1223
	 * mark the socket dead.
1224
	 *
1225
	 * @param Resource $sock The socket
1226
	 * @param int $len The number of bytes to read
1227
	 * @return string|bool The string on success, false on failure.
1228
	 */
1229
	function _fread( $sock, $len ) {
1230
		$buf = '';
1231
		while ( $len > 0 ) {
1232
			$result = fread( $sock, $len );
1233
			$data = stream_get_meta_data( $sock );
1234
			if ( $data['timed_out'] ) {
1235
				$this->_handle_error( $sock, 'timeout reading from $1' );
1236
				return false;
1237
			}
1238
			if ( $result === false ) {
1239
				$this->_handle_error( $sock, 'error reading buffer from $1' );
1240
				return false;
1241
			}
1242
			if ( $result === '' ) {
1243
				// This will happen if the remote end of the socket is shut down
1244
				$this->_handle_error( $sock, 'unexpected end of file reading from $1' );
1245
				return false;
1246
			}
1247
			$len -= strlen( $result );
1248
			$buf .= $result;
1249
		}
1250
		return $buf;
1251
	}
1252
1253
	/**
1254
	 * Read a line from a stream. If there is an error, mark the socket dead.
1255
	 * The \r\n line ending is stripped from the response.
1256
	 *
1257
	 * @param Resource $sock The socket
1258
	 * @return string|bool The string on success, false on failure
1259
	 */
1260
	function _fgets( $sock ) {
1261
		$result = fgets( $sock );
1262
		// fgets() may return a partial line if there is a select timeout after
1263
		// a successful recv(), so we have to check for a timeout even if we
1264
		// got a string response.
1265
		$data = stream_get_meta_data( $sock );
1266
		if ( $data['timed_out'] ) {
1267
			$this->_handle_error( $sock, 'timeout reading line from $1' );
1268
			return false;
1269
		}
1270
		if ( $result === false ) {
1271
			$this->_handle_error( $sock, 'error reading line from $1' );
1272
			return false;
1273
		}
1274
		if ( substr( $result, -2 ) === "\r\n" ) {
1275
			$result = substr( $result, 0, -2 );
1276 View Code Duplication
		} elseif ( substr( $result, -1 ) === "\n" ) {
1277
			$result = substr( $result, 0, -1 );
1278
		} else {
1279
			$this->_handle_error( $sock, 'line ending missing in response from $1' );
1280
			return false;
1281
		}
1282
		return $result;
1283
	}
1284
1285
	/**
1286
	 * Flush the read buffer of a stream
1287
	 * @param Resource $f
1288
	 */
1289
	function _flush_read_buffer( $f ) {
1290
		if ( !is_resource( $f ) ) {
1291
			return;
1292
		}
1293
		$r = array( $f );
1294
		$w = null;
1295
		$e = null;
1296
		$n = stream_select( $r, $w, $e, 0, 0 );
1297
		while ( $n == 1 && !feof( $f ) ) {
1298
			fread( $f, 1024 );
1299
			$r = array( $f );
1300
			$w = null;
1301
			$e = null;
1302
			$n = stream_select( $r, $w, $e, 0, 0 );
1303
		}
1304
	}
1305
1306
	// }}}
1307
	// }}}
1308
	// }}}
1309
}
1310
1311
// }}}
1312