[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Handle request startup, before loading the environment or libraries. This 5 * class bootstraps the request state up to the point where we can enter 6 * Phabricator code. 7 * 8 * NOTE: This class MUST NOT have any dependencies. It runs before libraries 9 * load. 10 * 11 * Rate Limiting 12 * ============= 13 * 14 * Phabricator limits the rate at which clients can request pages, and issues 15 * HTTP 429 "Too Many Requests" responses if clients request too many pages too 16 * quickly. Although this is not a complete defense against high-volume attacks, 17 * it can protect an install against aggressive crawlers, security scanners, 18 * and some types of malicious activity. 19 * 20 * To perform rate limiting, each page increments a score counter for the 21 * requesting user's IP. The page can give the IP more points for an expensive 22 * request, or fewer for an authetnicated request. 23 * 24 * Score counters are kept in buckets, and writes move to a new bucket every 25 * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}), 26 * the oldest bucket is discarded. This provides a simple mechanism for keeping 27 * track of scores without needing to store, access, or read very much data. 28 * 29 * Users are allowed to accumulate up to 1000 points per minute, averaged across 30 * all of the tracked buckets. 31 * 32 * @task info Accessing Request Information 33 * @task hook Startup Hooks 34 * @task apocalypse In Case Of Apocalypse 35 * @task validation Validation 36 * @task ratelimit Rate Limiting 37 */ 38 final class PhabricatorStartup { 39 40 private static $startTime; 41 private static $debugTimeLimit; 42 private static $globals = array(); 43 private static $capturingOutput; 44 private static $rawInput; 45 private static $oldMemoryLimit; 46 47 // TODO: For now, disable rate limiting entirely by default. We need to 48 // iterate on it a bit for Conduit, some of the specific score levels, and 49 // to deal with NAT'd offices. 50 private static $maximumRate = 0; 51 52 53 /* -( Accessing Request Information )-------------------------------------- */ 54 55 56 /** 57 * @task info 58 */ 59 public static function getStartTime() { 60 return self::$startTime; 61 } 62 63 64 /** 65 * @task info 66 */ 67 public static function getMicrosecondsSinceStart() { 68 return (int)(1000000 * (microtime(true) - self::getStartTime())); 69 } 70 71 72 /** 73 * @task info 74 */ 75 public static function setGlobal($key, $value) { 76 self::validateGlobal($key); 77 78 self::$globals[$key] = $value; 79 } 80 81 82 /** 83 * @task info 84 */ 85 public static function getGlobal($key, $default = null) { 86 self::validateGlobal($key); 87 88 if (!array_key_exists($key, self::$globals)) { 89 return $default; 90 } 91 92 return self::$globals[$key]; 93 } 94 95 /** 96 * @task info 97 */ 98 public static function getRawInput() { 99 return self::$rawInput; 100 } 101 102 103 /* -( Startup Hooks )------------------------------------------------------ */ 104 105 106 /** 107 * @task hook 108 */ 109 public static function didStartup() { 110 self::$startTime = microtime(true); 111 self::$globals = array(); 112 113 static $registered; 114 if (!$registered) { 115 // NOTE: This protects us against multiple calls to didStartup() in the 116 // same request, but also against repeated requests to the same 117 // interpreter state, which we may implement in the future. 118 register_shutdown_function(array(__CLASS__, 'didShutdown')); 119 $registered = true; 120 } 121 122 self::setupPHP(); 123 self::verifyPHP(); 124 125 if (isset($_SERVER['REMOTE_ADDR'])) { 126 self::rateLimitRequest($_SERVER['REMOTE_ADDR']); 127 } 128 129 self::normalizeInput(); 130 131 self::verifyRewriteRules(); 132 133 self::detectPostMaxSizeTriggered(); 134 135 self::beginOutputCapture(); 136 137 self::$rawInput = (string)file_get_contents('php://input'); 138 } 139 140 141 /** 142 * @task hook 143 */ 144 public static function didShutdown() { 145 $event = error_get_last(); 146 147 if (!$event) { 148 return; 149 } 150 151 switch ($event['type']) { 152 case E_ERROR: 153 case E_PARSE: 154 case E_COMPILE_ERROR: 155 break; 156 default: 157 return; 158 } 159 160 $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n"; 161 if ($event) { 162 // Even though we should be emitting this as text-plain, escape things 163 // just to be sure since we can't really be sure what the program state 164 // is when we get here. 165 $msg .= htmlspecialchars( 166 $event['message']."\n\n".$event['file'].':'.$event['line'], 167 ENT_QUOTES, 168 'UTF-8'); 169 } 170 171 // flip dem tables 172 $msg .= "\n\n\n"; 173 $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". 174 "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". 175 "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; 176 177 self::didFatal($msg); 178 } 179 180 public static function loadCoreLibraries() { 181 $phabricator_root = dirname(dirname(__FILE__)); 182 $libraries_root = dirname($phabricator_root); 183 184 $root = null; 185 if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { 186 $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; 187 } 188 189 ini_set( 190 'include_path', 191 $libraries_root.PATH_SEPARATOR.ini_get('include_path')); 192 193 @include_once $root.'libphutil/src/__phutil_library_init__.php'; 194 if (!@constant('__LIBPHUTIL__')) { 195 self::didFatal( 196 "Unable to load libphutil. Put libphutil/ next to phabricator/, or ". 197 "update your PHP 'include_path' to include the parent directory of ". 198 "libphutil/."); 199 } 200 201 phutil_load_library('arcanist/src'); 202 203 // Load Phabricator itself using the absolute path, so we never end up doing 204 // anything surprising (loading index.php and libraries from different 205 // directories). 206 phutil_load_library($phabricator_root.'/src'); 207 } 208 209 /* -( Output Capture )----------------------------------------------------- */ 210 211 212 public static function beginOutputCapture() { 213 if (self::$capturingOutput) { 214 self::didFatal('Already capturing output!'); 215 } 216 self::$capturingOutput = true; 217 ob_start(); 218 } 219 220 221 public static function endOutputCapture() { 222 if (!self::$capturingOutput) { 223 return null; 224 } 225 self::$capturingOutput = false; 226 return ob_get_clean(); 227 } 228 229 230 /* -( Debug Time Limit )--------------------------------------------------- */ 231 232 233 /** 234 * Set a time limit (in seconds) for the current script. After time expires, 235 * the script fatals. 236 * 237 * This works like `max_execution_time`, but prints out a useful stack trace 238 * when the time limit expires. This is primarily intended to make it easier 239 * to debug pages which hang by allowing extraction of a stack trace: set a 240 * short debug limit, then use the trace to figure out what's happening. 241 * 242 * The limit is implemented with a tick function, so enabling it implies 243 * some accounting overhead. 244 * 245 * @param int Time limit in seconds. 246 * @return void 247 */ 248 public static function setDebugTimeLimit($limit) { 249 self::$debugTimeLimit = $limit; 250 251 static $initialized; 252 if (!$initialized) { 253 declare(ticks=1); 254 register_tick_function(array('PhabricatorStartup', 'onDebugTick')); 255 } 256 } 257 258 259 /** 260 * Callback tick function used by @{method:setDebugTimeLimit}. 261 * 262 * Fatals with a useful stack trace after the time limit expires. 263 * 264 * @return void 265 */ 266 public static function onDebugTick() { 267 $limit = self::$debugTimeLimit; 268 if (!$limit) { 269 return; 270 } 271 272 $elapsed = (microtime(true) - self::getStartTime()); 273 if ($elapsed > $limit) { 274 $frames = array(); 275 foreach (debug_backtrace() as $frame) { 276 $file = isset($frame['file']) ? $frame['file'] : '-'; 277 $file = basename($file); 278 279 $line = isset($frame['line']) ? $frame['line'] : '-'; 280 $class = isset($frame['class']) ? $frame['class'].'->' : null; 281 $func = isset($frame['function']) ? $frame['function'].'()' : '?'; 282 283 $frames[] = "{$file}:{$line} {$class}{$func}"; 284 } 285 286 self::didFatal( 287 "Request aborted by debug time limit after {$limit} seconds.\n\n". 288 "STACK TRACE\n". 289 implode("\n", $frames)); 290 } 291 } 292 293 294 /* -( In Case of Apocalypse )---------------------------------------------- */ 295 296 297 /** 298 * Fatal the request completely in response to an exception, sending a plain 299 * text message to the client. Calls @{method:didFatal} internally. 300 * 301 * @param string Brief description of the exception context, like 302 * `"Rendering Exception"`. 303 * @param Exception The exception itself. 304 * @param bool True if it's okay to show the exception's stack trace 305 * to the user. The trace will always be logged. 306 * @return exit This method **does not return**. 307 * 308 * @task apocalypse 309 */ 310 public static function didEncounterFatalException( 311 $note, 312 Exception $ex, 313 $show_trace) { 314 315 $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage(); 316 317 $full_message = $message; 318 $full_message .= "\n\n"; 319 $full_message .= $ex->getTraceAsString(); 320 321 if ($show_trace) { 322 $message = $full_message; 323 } 324 325 self::didFatal($message, $full_message); 326 } 327 328 329 /** 330 * Fatal the request completely, sending a plain text message to the client. 331 * 332 * @param string Plain text message to send to the client. 333 * @param string Plain text message to send to the error log. If not 334 * provided, the client message is used. You can pass a more 335 * detailed message here (e.g., with stack traces) to avoid 336 * showing it to users. 337 * @return exit This method **does not return**. 338 * 339 * @task apocalypse 340 */ 341 public static function didFatal($message, $log_message = null) { 342 if ($log_message === null) { 343 $log_message = $message; 344 } 345 346 self::endOutputCapture(); 347 $access_log = self::getGlobal('log.access'); 348 349 if ($access_log) { 350 // We may end up here before the access log is initialized, e.g. from 351 // verifyPHP(). 352 $access_log->setData( 353 array( 354 'c' => 500, 355 )); 356 $access_log->write(); 357 } 358 359 header( 360 'Content-Type: text/plain; charset=utf-8', 361 $replace = true, 362 $http_error = 500); 363 364 error_log($log_message); 365 echo $message; 366 367 exit(1); 368 } 369 370 371 /* -( Validation )--------------------------------------------------------- */ 372 373 374 /** 375 * @task validation 376 */ 377 private static function setupPHP() { 378 error_reporting(E_ALL | E_STRICT); 379 self::$oldMemoryLimit = ini_get('memory_limit'); 380 ini_set('memory_limit', -1); 381 382 // If we have libxml, disable the incredibly dangerous entity loader. 383 if (function_exists('libxml_disable_entity_loader')) { 384 libxml_disable_entity_loader(true); 385 } 386 } 387 388 389 /** 390 * @task validation 391 */ 392 public static function getOldMemoryLimit() { 393 return self::$oldMemoryLimit; 394 } 395 396 /** 397 * @task validation 398 */ 399 private static function normalizeInput() { 400 // Replace superglobals with unfiltered versions, disrespect php.ini (we 401 // filter ourselves) 402 $filter = array(INPUT_GET, INPUT_POST, 403 INPUT_SERVER, INPUT_ENV, INPUT_COOKIE, 404 ); 405 foreach ($filter as $type) { 406 $filtered = filter_input_array($type, FILTER_UNSAFE_RAW); 407 if (!is_array($filtered)) { 408 continue; 409 } 410 switch ($type) { 411 case INPUT_SERVER: 412 $_SERVER = array_merge($_SERVER, $filtered); 413 break; 414 case INPUT_GET: 415 $_GET = array_merge($_GET, $filtered); 416 break; 417 case INPUT_COOKIE: 418 $_COOKIE = array_merge($_COOKIE, $filtered); 419 break; 420 case INPUT_POST: 421 $_POST = array_merge($_POST, $filtered); 422 break; 423 case INPUT_ENV; 424 $_ENV = array_merge($_ENV, $filtered); 425 break; 426 } 427 } 428 429 // rebuild $_REQUEST, respecting order declared in ini files 430 $order = ini_get('request_order'); 431 if (!$order) { 432 $order = ini_get('variables_order'); 433 } 434 if (!$order) { 435 // $_REQUEST will be empty, leave it alone 436 return; 437 } 438 $_REQUEST = array(); 439 for ($i = 0; $i < strlen($order); $i++) { 440 switch ($order[$i]) { 441 case 'G': 442 $_REQUEST = array_merge($_REQUEST, $_GET); 443 break; 444 case 'P': 445 $_REQUEST = array_merge($_REQUEST, $_POST); 446 break; 447 case 'C': 448 $_REQUEST = array_merge($_REQUEST, $_COOKIE); 449 break; 450 default: 451 // $_ENV and $_SERVER never go into $_REQUEST 452 break; 453 } 454 } 455 } 456 457 /** 458 * @task validation 459 */ 460 private static function verifyPHP() { 461 $required_version = '5.2.3'; 462 if (version_compare(PHP_VERSION, $required_version) < 0) { 463 self::didFatal( 464 "You are running PHP version '".PHP_VERSION."', which is older than ". 465 "the minimum version, '{$required_version}'. Update to at least ". 466 "'{$required_version}'."); 467 } 468 469 if (get_magic_quotes_gpc()) { 470 self::didFatal( 471 "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ". 472 "feature is 'highly discouraged' by PHP's developers and you must ". 473 "disable it to run Phabricator. Consult the PHP manual for ". 474 "instructions."); 475 } 476 477 if (extension_loaded('apc')) { 478 $apc_version = phpversion('apc'); 479 $known_bad = array( 480 '3.1.14' => true, 481 '3.1.15' => true, 482 '3.1.15-dev' => true, 483 ); 484 if (isset($known_bad[$apc_version])) { 485 self::didFatal( 486 "You have APC {$apc_version} installed. This version of APC is ". 487 "known to be bad, and does not work with Phabricator (it will ". 488 "cause Phabricator to fatal unrecoverably with nonsense errors). ". 489 "Downgrade to version 3.1.13."); 490 } 491 } 492 } 493 494 495 /** 496 * @task validation 497 */ 498 private static function verifyRewriteRules() { 499 if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) { 500 return; 501 } 502 503 if (php_sapi_name() == 'cli-server') { 504 // Compatibility with PHP 5.4+ built-in web server. 505 $url = parse_url($_SERVER['REQUEST_URI']); 506 $_REQUEST['__path__'] = $url['path']; 507 return; 508 } 509 510 if (!isset($_REQUEST['__path__'])) { 511 self::didFatal( 512 "Request parameter '__path__' is not set. Your rewrite rules ". 513 "are not configured correctly."); 514 } 515 516 if (!strlen($_REQUEST['__path__'])) { 517 self::didFatal( 518 "Request parameter '__path__' is set, but empty. Your rewrite rules ". 519 "are not configured correctly. The '__path__' should always ". 520 "begin with a '/'."); 521 } 522 } 523 524 525 /** 526 * @task validation 527 */ 528 private static function validateGlobal($key) { 529 static $globals = array( 530 'log.access' => true, 531 'csrf.salt' => true, 532 ); 533 534 if (empty($globals[$key])) { 535 throw new Exception("Access to unknown startup global '{$key}'!"); 536 } 537 } 538 539 540 /** 541 * Detect if this request has had its POST data stripped by exceeding the 542 * 'post_max_size' PHP configuration limit. 543 * 544 * PHP has a setting called 'post_max_size'. If a POST request arrives with 545 * a body larger than the limit, PHP doesn't generate $_POST but processes 546 * the request anyway, and provides no formal way to detect that this 547 * happened. 548 * 549 * We can still read the entire body out of `php://input`. However according 550 * to the documentation the stream isn't available for "multipart/form-data" 551 * (on nginx + php-fpm it appears that it is available, though, at least) so 552 * any attempt to generate $_POST would be fragile. 553 * 554 * @task validation 555 */ 556 private static function detectPostMaxSizeTriggered() { 557 // If this wasn't a POST, we're fine. 558 if ($_SERVER['REQUEST_METHOD'] != 'POST') { 559 return; 560 } 561 562 // If there's POST data, clearly we're in good shape. 563 if ($_POST) { 564 return; 565 } 566 567 // For HTML5 drag-and-drop file uploads, Safari submits the data as 568 // "application/x-www-form-urlencoded". For most files this generates 569 // something in POST because most files decode to some nonempty (albeit 570 // meaningless) value. However, some files (particularly small images) 571 // don't decode to anything. If we know this is a drag-and-drop upload, 572 // we can skip this check. 573 if (isset($_REQUEST['__upload__'])) { 574 return; 575 } 576 577 // PHP generates $_POST only for two content types. This routing happens 578 // in `main/php_content_types.c` in PHP. Normally, all forms use one of 579 // these content types, but some requests may not -- for example, Firefox 580 // submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type 581 // of the file itself. If we don't have a recognized content type, we 582 // don't need $_POST. 583 // 584 // NOTE: We use strncmp() because the actual content type may be something 585 // like "multipart/form-data; boundary=...". 586 // 587 // NOTE: Chrome sometimes omits this header, see some discussion in T1762 588 // and http://code.google.com/p/chromium/issues/detail?id=6800 589 $content_type = isset($_SERVER['CONTENT_TYPE']) 590 ? $_SERVER['CONTENT_TYPE'] 591 : ''; 592 593 $parsed_types = array( 594 'application/x-www-form-urlencoded', 595 'multipart/form-data', 596 ); 597 598 $is_parsed_type = false; 599 foreach ($parsed_types as $parsed_type) { 600 if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) { 601 $is_parsed_type = true; 602 break; 603 } 604 } 605 606 if (!$is_parsed_type) { 607 return; 608 } 609 610 // Check for 'Content-Length'. If there's no data, we don't expect $_POST 611 // to exist. 612 $length = (int)$_SERVER['CONTENT_LENGTH']; 613 if (!$length) { 614 return; 615 } 616 617 // Time to fatal: we know this was a POST with data that should have been 618 // populated into $_POST, but it wasn't. 619 620 $config = ini_get('post_max_size'); 621 PhabricatorStartup::didFatal( 622 "As received by the server, this request had a nonzero content length ". 623 "but no POST data.\n\n". 624 "Normally, this indicates that it exceeds the 'post_max_size' setting ". 625 "in the PHP configuration on the server. Increase the 'post_max_size' ". 626 "setting or reduce the size of the request.\n\n". 627 "Request size according to 'Content-Length' was '{$length}', ". 628 "'post_max_size' is set to '{$config}'."); 629 } 630 631 632 /* -( Rate Limiting )------------------------------------------------------ */ 633 634 635 /** 636 * Adjust the permissible rate limit score. 637 * 638 * By default, the limit is `1000`. You can use this method to set it to 639 * a larger or smaller value. If you set it to `2000`, users may make twice 640 * as many requests before rate limiting. 641 * 642 * @param int Maximum score before rate limiting. 643 * @return void 644 * @task ratelimit 645 */ 646 public static function setMaximumRate($rate) { 647 self::$maximumRate = $rate; 648 } 649 650 651 /** 652 * Check if the user (identified by `$user_identity`) has issued too many 653 * requests recently. If they have, end the request with a 429 error code. 654 * 655 * The key just needs to identify the user. Phabricator uses both user PHIDs 656 * and user IPs as keys, tracking logged-in and logged-out users separately 657 * and enforcing different limits. 658 * 659 * @param string Some key which identifies the user making the request. 660 * @return void If the user has exceeded the rate limit, this method 661 * does not return. 662 * @task ratelimit 663 */ 664 public static function rateLimitRequest($user_identity) { 665 if (!self::canRateLimit()) { 666 return; 667 } 668 669 $score = self::getRateLimitScore($user_identity); 670 if ($score > (self::$maximumRate * self::getRateLimitBucketCount())) { 671 // Give the user some bonus points for getting rate limited. This keeps 672 // bad actors who keep slamming the 429 page locked out completely, 673 // instead of letting them get a burst of requests through every minute 674 // after a bucket expires. 675 self::addRateLimitScore($user_identity, 50); 676 self::didRateLimit($user_identity); 677 } 678 } 679 680 681 /** 682 * Add points to the rate limit score for some user. 683 * 684 * If users have earned more than 1000 points per minute across all the 685 * buckets they'll be locked out of the application, so awarding 1 point per 686 * request roughly corresponds to allowing 1000 requests per second, while 687 * awarding 50 points roughly corresponds to allowing 20 requests per second. 688 * 689 * @param string Some key which identifies the user making the request. 690 * @param float The cost for this request; more points pushes them toward 691 * the limit faster. 692 * @return void 693 * @task ratelimit 694 */ 695 public static function addRateLimitScore($user_identity, $score) { 696 if (!self::canRateLimit()) { 697 return; 698 } 699 700 $current = self::getRateLimitBucket(); 701 702 // There's a bit of a race here, if a second process reads the bucket before 703 // this one writes it, but it's fine if we occasionally fail to record a 704 // user's score. If they're making requests fast enough to hit rate 705 // limiting, we'll get them soon. 706 707 $bucket_key = self::getRateLimitBucketKey($current); 708 $bucket = apc_fetch($bucket_key); 709 if (!is_array($bucket)) { 710 $bucket = array(); 711 } 712 713 if (empty($bucket[$user_identity])) { 714 $bucket[$user_identity] = 0; 715 } 716 717 $bucket[$user_identity] += $score; 718 apc_store($bucket_key, $bucket); 719 } 720 721 722 /** 723 * Determine if rate limiting is available. 724 * 725 * Rate limiting depends on APC, and isn't available unless the APC user 726 * cache is available. 727 * 728 * @return bool True if rate limiting is available. 729 * @task ratelimit 730 */ 731 private static function canRateLimit() { 732 if (!self::$maximumRate) { 733 return false; 734 } 735 736 if (!function_exists('apc_fetch')) { 737 return false; 738 } 739 740 return true; 741 } 742 743 744 /** 745 * Get the current bucket for storing rate limit scores. 746 * 747 * @return int The current bucket. 748 * @task ratelimit 749 */ 750 private static function getRateLimitBucket() { 751 return (int)(time() / 60); 752 } 753 754 755 /** 756 * Get the total number of rate limit buckets to retain. 757 * 758 * @return int Total number of rate limit buckets to retain. 759 * @task ratelimit 760 */ 761 private static function getRateLimitBucketCount() { 762 return 5; 763 } 764 765 766 /** 767 * Get the APC key for a given bucket. 768 * 769 * @param int Bucket to get the key for. 770 * @return string APC key for the bucket. 771 * @task ratelimit 772 */ 773 private static function getRateLimitBucketKey($bucket) { 774 return 'rate:bucket:'.$bucket; 775 } 776 777 778 /** 779 * Get the APC key for the smallest stored bucket. 780 * 781 * @return string APC key for the smallest stored bucket. 782 * @task ratelimit 783 */ 784 private static function getRateLimitMinKey() { 785 return 'rate:min'; 786 } 787 788 789 /** 790 * Get the current rate limit score for a given user. 791 * 792 * @param string Unique key identifying the user. 793 * @return float The user's current score. 794 * @task ratelimit 795 */ 796 private static function getRateLimitScore($user_identity) { 797 $min_key = self::getRateLimitMinKey(); 798 799 // Identify the oldest bucket stored in APC. 800 $cur = self::getRateLimitBucket(); 801 $min = apc_fetch($min_key); 802 803 // If we don't have any buckets stored yet, store the current bucket as 804 // the oldest bucket. 805 if (!$min) { 806 apc_store($min_key, $cur); 807 $min = $cur; 808 } 809 810 // Destroy any buckets that are older than the minimum bucket we're keeping 811 // track of. Under load this normally shouldn't do anything, but will clean 812 // up an old bucket once per minute. 813 $count = self::getRateLimitBucketCount(); 814 for ($cursor = $min; $cursor < ($cur - $count); $cursor++) { 815 apc_delete(self::getRateLimitBucketKey($cursor)); 816 apc_store($min_key, $cursor + 1); 817 } 818 819 // Now, sum up the user's scores in all of the active buckets. 820 $score = 0; 821 for (; $cursor <= $cur; $cursor++) { 822 $bucket = apc_fetch(self::getRateLimitBucketKey($cursor)); 823 if (isset($bucket[$user_identity])) { 824 $score += $bucket[$user_identity]; 825 } 826 } 827 828 return $score; 829 } 830 831 832 /** 833 * Emit an HTTP 429 "Too Many Requests" response (indicating that the user 834 * has exceeded application rate limits) and exit. 835 * 836 * @return exit This method **does not return**. 837 * @task ratelimit 838 */ 839 private static function didRateLimit() { 840 $message = 841 "TOO MANY REQUESTS\n". 842 "You are issuing too many requests too quickly.\n". 843 "To adjust limits, see \"Configuring a Preamble Script\" in the ". 844 "documentation."; 845 846 header( 847 'Content-Type: text/plain; charset=utf-8', 848 $replace = true, 849 $http_error = 429); 850 851 echo $message; 852 853 exit(1); 854 } 855 856 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |