@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 882 lines 28 kB view raw
1<?php 2 3/** 4 * @task routing URI Routing 5 * @task response Response Handling 6 * @task exception Exception Handling 7 */ 8final class AphrontApplicationConfiguration 9 extends Phobject { 10 11 private $request; 12 private $host; 13 private $path; 14 private $console; 15 16 public function buildRequest() { 17 $parser = new PhutilQueryStringParser(); 18 19 $data = array(); 20 $data += $_POST; 21 $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); 22 23 $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); 24 25 $request = new AphrontRequest($this->getHost(), $this->getPath()); 26 $request->setRequestData($data); 27 $request->setApplicationConfiguration($this); 28 $request->setCookiePrefix($cookie_prefix); 29 30 $request->updateEphemeralCookies(); 31 32 return $request; 33 } 34 35 public function buildRedirectController($uri, $external) { 36 return array( 37 new PhabricatorRedirectController(), 38 array( 39 'uri' => $uri, 40 'external' => $external, 41 ), 42 ); 43 } 44 45 public function setRequest(AphrontRequest $request) { 46 $this->request = $request; 47 return $this; 48 } 49 50 public function getRequest() { 51 return $this->request; 52 } 53 54 public function getConsole() { 55 return $this->console; 56 } 57 58 public function setConsole($console) { 59 $this->console = $console; 60 return $this; 61 } 62 63 public function setHost($host) { 64 $this->host = $host; 65 return $this; 66 } 67 68 public function getHost() { 69 return $this->host; 70 } 71 72 public function setPath($path) { 73 $this->path = $path; 74 return $this; 75 } 76 77 public function getPath() { 78 return $this->path; 79 } 80 81 82 /** 83 * @phutil-external-symbol class PhabricatorStartup 84 */ 85 public static function runHTTPRequest(AphrontHTTPSink $sink) { 86 if (isset($_SERVER['HTTP_X_SETUP_SELFCHECK'])) { 87 $response = self::newSelfCheckResponse(); 88 return self::writeResponse($sink, $response); 89 } 90 91 PhabricatorStartup::beginStartupPhase('multimeter'); 92 $multimeter = MultimeterControl::newInstance(); 93 $multimeter->setEventContext('<http-init>'); 94 $multimeter->setEventViewer('<none>'); 95 96 // Build a no-op write guard for the setup phase. We'll replace this with a 97 // real write guard later on, but we need to survive setup and build a 98 // request object first. 99 $write_guard = new AphrontWriteGuard('id'); 100 101 PhabricatorStartup::beginStartupPhase('preflight'); 102 103 $response = PhabricatorSetupCheck::willPreflightRequest(); 104 if ($response) { 105 return self::writeResponse($sink, $response); 106 } 107 108 PhabricatorStartup::beginStartupPhase('env.init'); 109 110 self::readHTTPPOSTData(); 111 112 try { 113 PhabricatorEnv::initializeWebEnvironment(); 114 $database_exception = null; 115 } catch (PhabricatorClusterStrandedException $ex) { 116 $database_exception = $ex; 117 } 118 119 // If we're in developer mode, set a flag so that top-level exception 120 // handlers can add more information. 121 if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { 122 $sink->setShowStackTraces(true); 123 } 124 125 if ($database_exception) { 126 $issue = PhabricatorSetupIssue::newDatabaseConnectionIssue( 127 $database_exception, 128 true); 129 $response = PhabricatorSetupCheck::newIssueResponse($issue); 130 return self::writeResponse($sink, $response); 131 } 132 133 $multimeter->setSampleRate( 134 PhabricatorEnv::getEnvConfig('debug.sample-rate')); 135 136 $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit'); 137 if ($debug_time_limit) { 138 PhabricatorStartup::setDebugTimeLimit($debug_time_limit); 139 } 140 141 // This is the earliest we can get away with this, we need env config first. 142 PhabricatorStartup::beginStartupPhase('log.access'); 143 PhabricatorAccessLog::init(); 144 $access_log = PhabricatorAccessLog::getLog(); 145 PhabricatorStartup::setAccessLog($access_log); 146 147 $address = PhabricatorEnv::getRemoteAddress(); 148 if ($address) { 149 $address_string = $address->getAddress(); 150 } else { 151 $address_string = '-'; 152 } 153 154 $access_log->setData( 155 array( 156 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), 157 'r' => $address_string, 158 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), 159 )); 160 161 DarkConsoleXHProfPluginAPI::hookProfiler(); 162 163 // We just activated the profiler, so we don't need to keep track of 164 // startup phases anymore: it can take over from here. 165 PhabricatorStartup::beginStartupPhase('startup.done'); 166 167 DarkConsoleErrorLogPluginAPI::registerErrorHandler(); 168 169 $response = PhabricatorSetupCheck::willProcessRequest(); 170 if ($response) { 171 return self::writeResponse($sink, $response); 172 } 173 174 $host = AphrontRequest::getHTTPHeader('Host'); 175 $path = PhabricatorStartup::getRequestPath(); 176 177 $application = new self(); 178 179 $application->setHost($host); 180 $application->setPath($path); 181 $request = $application->buildRequest(); 182 183 // Now that we have a request, convert the write guard into one which 184 // actually checks CSRF tokens. 185 $write_guard->dispose(); 186 $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); 187 188 // Build the server URI implied by the request headers. If an administrator 189 // has not configured "phabricator.base-uri" yet, we'll use this to generate 190 // links. 191 192 $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); 193 $request_base_uri = "{$request_protocol}://{$host}/"; 194 PhabricatorEnv::setRequestBaseURI($request_base_uri); 195 196 $access_log->setData( 197 array( 198 'U' => (string)$request->getRequestURI()->getPath(), 199 )); 200 201 $processing_exception = null; 202 try { 203 $response = $application->processRequest( 204 $request, 205 $access_log, 206 $sink, 207 $multimeter); 208 $response_code = $response->getHTTPResponseCode(); 209 } catch (Exception $ex) { 210 $processing_exception = $ex; 211 $response_code = 500; 212 } 213 214 $write_guard->dispose(); 215 216 $access_log->setData( 217 array( 218 'c' => $response_code, 219 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), 220 )); 221 222 $multimeter->newEvent( 223 MultimeterEvent::TYPE_REQUEST_TIME, 224 $multimeter->getEventContext(), 225 PhabricatorStartup::getMicrosecondsSinceStart()); 226 227 $access_log->write(); 228 229 $multimeter->saveEvents(); 230 231 DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); 232 233 PhabricatorStartup::disconnectRateLimits( 234 array( 235 'viewer' => $request->getUser(), 236 )); 237 238 if ($processing_exception) { 239 throw $processing_exception; 240 } 241 } 242 243 244 public function processRequest( 245 AphrontRequest $request, 246 PhutilDeferredLog $access_log, 247 AphrontHTTPSink $sink, 248 MultimeterControl $multimeter) { 249 250 $this->setRequest($request); 251 252 list($controller, $uri_data) = $this->buildController(); 253 254 $controller_class = get_class($controller); 255 $access_log->setData( 256 array( 257 'C' => $controller_class, 258 )); 259 $multimeter->setEventContext('web.'.$controller_class); 260 261 $request->setController($controller); 262 $request->setURIMap($uri_data); 263 264 $controller->setRequest($request); 265 266 // If execution throws an exception and then trying to render that 267 // exception throws another exception, we want to show the original 268 // exception, as it is likely the root cause of the rendering exception. 269 $original_exception = null; 270 try { 271 $response = $controller->willBeginExecution(); 272 273 if ($request->getUser() && $request->getUser()->getPHID()) { 274 $access_log->setData( 275 array( 276 'u' => $request->getUser()->getUserName(), 277 'P' => $request->getUser()->getPHID(), 278 )); 279 $multimeter->setEventViewer('user.'.$request->getUser()->getPHID()); 280 } 281 282 if (!$response) { 283 $controller->willProcessRequest($uri_data); 284 $response = $controller->handleRequest($request); 285 $this->validateControllerResponse($controller, $response); 286 } 287 } catch (Exception $ex) { 288 $original_exception = $ex; 289 } catch (Throwable $ex) { 290 $original_exception = $ex; 291 } 292 293 $response_exception = null; 294 try { 295 if ($original_exception) { 296 $response = $this->handleThrowable($original_exception); 297 } 298 299 $response = $this->produceResponse($request, $response); 300 $response = $controller->willSendResponse($response); 301 $response->setRequest($request); 302 303 self::writeResponse($sink, $response); 304 } catch (Exception $ex) { 305 $response_exception = $ex; 306 } catch (Throwable $ex) { 307 $response_exception = $ex; 308 } 309 310 if ($response_exception) { 311 // If we encountered an exception while building a normal response, then 312 // encountered another exception while building a response for the first 313 // exception, throw an aggregate exception that will be unpacked by the 314 // higher-level handler. This is above our pay grade. 315 if ($original_exception) { 316 throw new PhutilAggregateException( 317 pht( 318 'Encountered a processing exception, then another exception when '. 319 'trying to build a response for the first exception.'), 320 array( 321 $response_exception, 322 $original_exception, 323 )); 324 } 325 326 // If we built a response successfully and then ran into an exception 327 // trying to render it, try to handle and present that exception to the 328 // user using the standard handler. 329 330 // The problem here might be in rendering (more common) or in the actual 331 // response mechanism (less common). If it's in rendering, we can likely 332 // still render a nice exception page: the majority of rendering issues 333 // are in main page content, not content shared with the exception page. 334 335 $handling_exception = null; 336 try { 337 $response = $this->handleThrowable($response_exception); 338 339 $response = $this->produceResponse($request, $response); 340 $response = $controller->willSendResponse($response); 341 $response->setRequest($request); 342 343 self::writeResponse($sink, $response); 344 } catch (Exception $ex) { 345 $handling_exception = $ex; 346 } catch (Throwable $ex) { 347 $handling_exception = $ex; 348 } 349 350 // If we didn't have any luck with that, raise the original response 351 // exception. As above, this is the root cause exception and more likely 352 // to be useful. This will go to the fallback error handler at top 353 // level. 354 355 if ($handling_exception) { 356 throw $response_exception; 357 } 358 } 359 360 return $response; 361 } 362 363 private static function writeResponse( 364 AphrontHTTPSink $sink, 365 AphrontResponse $response) { 366 367 $unexpected_output = PhabricatorStartup::endOutputCapture(); 368 if ($unexpected_output) { 369 $unexpected_output = pht( 370 "Unexpected output:\n\n%s", 371 $unexpected_output); 372 373 phlog($unexpected_output); 374 375 if ($response instanceof AphrontWebpageResponse) { 376 $response->setUnexpectedOutput($unexpected_output); 377 } 378 } 379 380 $sink->writeResponse($response); 381 } 382 383 384/* -( URI Routing )-------------------------------------------------------- */ 385 386 387 /** 388 * Build a controller to respond to the request. 389 * 390 * @return array<AphrontController,array> Controller and dictionary of 391 * request parameters. 392 * @task routing 393 */ 394 private function buildController() { 395 $request = $this->getRequest(); 396 397 // If we're configured to operate in cluster mode, reject requests which 398 // were not received on a cluster interface. 399 // 400 // For example, a host may have an internal address like "170.0.0.1", and 401 // also have a public address like "51.23.95.16". Assuming the cluster 402 // is configured on a range like "170.0.0.0/16", we want to reject the 403 // requests received on the public interface. 404 // 405 // Ideally, nodes in a cluster should only be listening on internal 406 // interfaces, but they may be configured in such a way that they also 407 // listen on external interfaces, since this is easy to forget about or 408 // get wrong. As a broad security measure, reject requests received on any 409 // interfaces which aren't on the whitelist. 410 411 $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); 412 if ($cluster_addresses) { 413 $server_addr = idx($_SERVER, 'SERVER_ADDR'); 414 if (!$server_addr) { 415 if (php_sapi_name() == 'cli') { 416 // This is a command line script (probably something like a unit 417 // test) so it's fine that we don't have SERVER_ADDR defined. 418 } else { 419 throw new AphrontMalformedRequestException( 420 pht('No %s', 'SERVER_ADDR'), 421 pht( 422 'This service is configured to operate in cluster mode, but '. 423 '%s is not defined in the request context. Your webserver '. 424 'configuration needs to forward %s to PHP so the software can '. 425 'reject requests received on external interfaces.', 426 'SERVER_ADDR', 427 'SERVER_ADDR')); 428 } 429 } else { 430 if (!PhabricatorEnv::isClusterAddress($server_addr)) { 431 throw new AphrontMalformedRequestException( 432 pht('External Interface'), 433 pht( 434 'This service is configured in cluster mode and the address '. 435 'this request was received on ("%s") is not whitelisted as '. 436 'a cluster address.', 437 $server_addr)); 438 } 439 } 440 } 441 442 $site = $this->buildSiteForRequest($request); 443 444 if ($site->shouldRequireHTTPS()) { 445 if (!$request->isHTTPS()) { 446 447 // Don't redirect intracluster requests: doing so drops headers and 448 // parameters, imposes a performance penalty, and indicates a 449 // misconfiguration. 450 if ($request->isProxiedClusterRequest()) { 451 throw new AphrontMalformedRequestException( 452 pht('HTTPS Required'), 453 pht( 454 'This request reached a site which requires HTTPS, but the '. 455 'request is not marked as HTTPS.')); 456 } 457 458 $https_uri = $request->getRequestURI(); 459 $https_uri->setDomain($request->getHost()); 460 $https_uri->setProtocol('https'); 461 462 // In this scenario, we'll be redirecting to HTTPS using an absolute 463 // URI, so we need to permit an external redirect. 464 return $this->buildRedirectController($https_uri, true); 465 } 466 } 467 468 $maps = $site->getRoutingMaps(); 469 $path = $request->getPath(); 470 471 $result = $this->routePath($maps, $path); 472 if ($result) { 473 return $result; 474 } 475 476 // If we failed to match anything but don't have a trailing slash, try 477 // to add a trailing slash and issue a redirect if that resolves. 478 479 // NOTE: We only do this for GET, since redirects switch to GET and drop 480 // data like POST parameters. 481 if (!preg_match('@/$@', $path) && $request->isHTTPGet()) { 482 $result = $this->routePath($maps, $path.'/'); 483 if ($result) { 484 $target_uri = $request->getAbsoluteRequestURI(); 485 486 // We need to restore URI encoding because the webserver has 487 // interpreted it. For example, this allows us to redirect a path 488 // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be 489 // resolved meaningfully by an application. 490 $target_path = phutil_escape_uri($path.'/'); 491 $target_uri->setPath($target_path); 492 $target_uri = (string)$target_uri; 493 494 return $this->buildRedirectController($target_uri, true); 495 } 496 } 497 498 $result = $site->new404Controller($request); 499 if ($result) { 500 return array($result, array()); 501 } 502 503 throw new Exception( 504 pht( 505 'Aphront site ("%s") failed to build a 404 controller.', 506 get_class($site))); 507 } 508 509 /** 510 * Map a specific path to the corresponding controller. For a description 511 * of routing, see @{method:buildController}. 512 * 513 * @param list<AphrontRoutingMap> $maps List of routing maps. 514 * @param string $path Path to route. 515 * @return array<AphrontController,array<string,string>>|null Controller 516 * subclass and dictionary of request parameters, or null if no paths to 517 * route were found. 518 * @task routing 519 */ 520 private function routePath(array $maps, $path) { 521 foreach ($maps as $map) { 522 $result = $map->routePath($path); 523 if ($result) { 524 return array($result->getController(), $result->getURIData()); 525 } 526 } 527 return null; 528 } 529 530 private function buildSiteForRequest(AphrontRequest $request) { 531 $sites = PhabricatorSite::getAllSites(); 532 533 $site = null; 534 foreach ($sites as $candidate) { 535 $site = $candidate->newSiteForRequest($request); 536 if ($site) { 537 break; 538 } 539 } 540 541 if (!$site) { 542 $path = $request->getPath(); 543 $host = $request->getHost(); 544 throw new AphrontMalformedRequestException( 545 pht('Site Not Found'), 546 pht( 547 'This request asked for "%s" on host "%s", but no site is '. 548 'configured which can serve this request.', 549 $path, 550 $host), 551 true); 552 } 553 554 $request->setSite($site); 555 556 return $site; 557 } 558 559 560/* -( Response Handling )-------------------------------------------------- */ 561 562 563 /** 564 * Tests if a response is of a valid type. 565 * 566 * @param mixed $response Supposedly valid response. 567 * @return bool True if the object is of a valid type. 568 * @task response 569 */ 570 private function isValidResponseObject($response) { 571 if ($response instanceof AphrontResponse) { 572 return true; 573 } 574 575 if ($response instanceof AphrontResponseProducerInterface) { 576 return true; 577 } 578 579 return false; 580 } 581 582 583 /** 584 * Verifies that the return value from an @{class:AphrontController} is 585 * of an allowed type. 586 * 587 * @param AphrontController $controller Controller which returned the 588 * response. 589 * @param mixed $response Supposedly valid response. 590 * @return void 591 * @task response 592 */ 593 private function validateControllerResponse( 594 AphrontController $controller, 595 $response) { 596 597 if ($this->isValidResponseObject($response)) { 598 return; 599 } 600 601 throw new Exception( 602 pht( 603 'Controller "%s" returned an invalid response from call to "%s". '. 604 'This method must return an object of class "%s", or an object '. 605 'which implements the "%s" interface.', 606 get_class($controller), 607 'handleRequest()', 608 'AphrontResponse', 609 'AphrontResponseProducerInterface')); 610 } 611 612 613 /** 614 * Verifies that the return value from an 615 * @{class:AphrontResponseProducerInterface} is of an allowed type. 616 * 617 * @param AphrontResponseProducerInterface $producer Object which produced 618 * this response. 619 * @param mixed $response Supposedly valid response. 620 * @return void 621 * @task response 622 */ 623 private function validateProducerResponse( 624 AphrontResponseProducerInterface $producer, 625 $response) { 626 627 if ($this->isValidResponseObject($response)) { 628 return; 629 } 630 631 throw new Exception( 632 pht( 633 'Producer "%s" returned an invalid response from call to "%s". '. 634 'This method must return an object of class "%s", or an object '. 635 'which implements the "%s" interface.', 636 get_class($producer), 637 'produceAphrontResponse()', 638 'AphrontResponse', 639 'AphrontResponseProducerInterface')); 640 } 641 642 643 /** 644 * Verifies that the return value from an 645 * @{class:AphrontRequestExceptionHandler} is of an allowed type. 646 * 647 * @param AphrontRequestExceptionHandler $handler Object which produced this 648 * response. 649 * @param mixed $response Supposedly valid response. 650 * @return void 651 * @task response 652 */ 653 private function validateErrorHandlerResponse( 654 AphrontRequestExceptionHandler $handler, 655 $response) { 656 657 if ($this->isValidResponseObject($response)) { 658 return; 659 } 660 661 throw new Exception( 662 pht( 663 'Exception handler "%s" returned an invalid response from call to '. 664 '"%s". This method must return an object of class "%s", or an object '. 665 'which implements the "%s" interface.', 666 get_class($handler), 667 'handleRequestException()', 668 'AphrontResponse', 669 'AphrontResponseProducerInterface')); 670 } 671 672 673 /** 674 * Resolves a response object into an @{class:AphrontResponse}. 675 * 676 * Controllers are permitted to return actual responses of class 677 * @{class:AphrontResponse}, or other objects which implement 678 * @{interface:AphrontResponseProducerInterface} and can produce a response. 679 * 680 * If a controller returns a response producer, invoke it now and produce 681 * the real response. 682 * 683 * @param AphrontRequest $request Request being handled. 684 * @param AphrontResponse|AphrontResponseProducerInterface $response 685 * Response, or response producer. 686 * @return AphrontResponse Response after any required production. 687 * @task response 688 */ 689 private function produceResponse(AphrontRequest $request, $response) { 690 $original = $response; 691 692 // Detect cycles on the exact same objects. It's still possible to produce 693 // infinite responses as long as they're all unique, but we can only 694 // reasonably detect cycles, not guarantee that response production halts. 695 696 $seen = array(); 697 while (true) { 698 // NOTE: It is permissible for an object to be both a response and a 699 // response producer. If so, being a producer is "stronger". This is 700 // used by AphrontProxyResponse. 701 702 // If this response is a valid response, hand over the request first. 703 if ($response instanceof AphrontResponse) { 704 $response->setRequest($request); 705 } 706 707 // If this isn't a producer, we're all done. 708 if (!($response instanceof AphrontResponseProducerInterface)) { 709 break; 710 } 711 712 $hash = spl_object_hash($response); 713 if (isset($seen[$hash])) { 714 throw new Exception( 715 pht( 716 'Failure while producing response for object of class "%s": '. 717 'encountered production cycle (identical object, of class "%s", '. 718 'was produced twice).', 719 get_class($original), 720 get_class($response))); 721 } 722 723 $seen[$hash] = true; 724 725 $new_response = $response->produceAphrontResponse(); 726 $this->validateProducerResponse($response, $new_response); 727 $response = $new_response; 728 } 729 730 return $response; 731 } 732 733 734/* -( Error Handling )----------------------------------------------------- */ 735 736 737 /** 738 * Convert an exception which has escaped the controller into a response. 739 * 740 * This method delegates exception handling to available subclasses of 741 * @{class:AphrontRequestExceptionHandler}. 742 * 743 * @param Throwable $throwable Exception which needs to be handled. 744 * @return mixed Response or response producer, or null if no available 745 * handler can produce a response. 746 * @task exception 747 */ 748 private function handleThrowable($throwable) { 749 $handlers = AphrontRequestExceptionHandler::getAllHandlers(); 750 751 $request = $this->getRequest(); 752 foreach ($handlers as $handler) { 753 if ($handler->canHandleRequestThrowable($request, $throwable)) { 754 $response = $handler->handleRequestThrowable($request, $throwable); 755 $this->validateErrorHandlerResponse($handler, $response); 756 return $response; 757 } 758 } 759 760 throw $throwable; 761 } 762 763 private static function newSelfCheckResponse() { 764 $path = PhabricatorStartup::getRequestPath(); 765 $query = idx($_SERVER, 'QUERY_STRING', ''); 766 767 $pairs = id(new PhutilQueryStringParser()) 768 ->parseQueryStringToPairList($query); 769 770 $params = array(); 771 foreach ($pairs as $v) { 772 $params[] = array( 773 'name' => $v[0], 774 'value' => $v[1], 775 ); 776 } 777 778 $raw_input = @file_get_contents('php://input'); 779 if ($raw_input !== false) { 780 $base64_input = base64_encode($raw_input); 781 } else { 782 $base64_input = null; 783 } 784 785 $result = array( 786 'path' => $path, 787 'params' => $params, 788 'user' => idx($_SERVER, 'PHP_AUTH_USER'), 789 'pass' => idx($_SERVER, 'PHP_AUTH_PW'), 790 791 'raw.base64' => $base64_input, 792 793 // This just makes sure that the response compresses well, so reasonable 794 // algorithms should want to gzip or deflate it. 795 'filler' => str_repeat('Q', 1024 * 16), 796 ); 797 798 return id(new AphrontJSONResponse()) 799 ->setAddJSONShield(false) 800 ->setContent($result); 801 } 802 803 private static function readHTTPPOSTData() { 804 $request_method = idx($_SERVER, 'REQUEST_METHOD'); 805 if ($request_method === 'PUT') { 806 // For PUT requests, do nothing: in particular, do NOT read input. This 807 // allows us to stream input later and process very large PUT requests, 808 // like those coming from Git LFS. 809 return; 810 } 811 812 813 // For POST requests, we're going to read the raw input ourselves here 814 // if we can. Among other things, this corrects variable names with 815 // the "." character in them, which PHP normally converts into "_". 816 817 // If "enable_post_data_reading" is on, the documentation suggests we 818 // can not read the body. In practice, we seem to be able to. This may 819 // need to be resolved at some point, likely by instructing installs 820 // to disable this option. 821 822 // If the content type is "multipart/form-data", we need to build both 823 // $_POST and $_FILES, which is involved. The body itself is also more 824 // difficult to parse than other requests. 825 826 $raw_input = PhabricatorStartup::getRawInput(); 827 $parser = new PhutilQueryStringParser(); 828 829 if (phutil_nonempty_string($raw_input)) { 830 $content_type = idx($_SERVER, 'CONTENT_TYPE'); 831 $is_multipart = preg_match('@^multipart/form-data@i', $content_type); 832 if ($is_multipart) { 833 $multipart_parser = id(new AphrontMultipartParser()) 834 ->setContentType($content_type); 835 836 $multipart_parser->beginParse(); 837 $multipart_parser->continueParse($raw_input); 838 $parts = $multipart_parser->endParse(); 839 840 // We're building and then parsing a query string so that requests 841 // with arrays (like "x[]=apple&x[]=banana") work correctly. This also 842 // means we can't use "phutil_build_http_querystring()", since it 843 // can't build a query string with duplicate names. 844 845 $query_string = array(); 846 foreach ($parts as $part) { 847 if (!$part->isVariable()) { 848 continue; 849 } 850 851 $name = $part->getName(); 852 $value = $part->getVariableValue(); 853 $query_string[] = rawurlencode($name).'='.rawurlencode($value); 854 } 855 $query_string = implode('&', $query_string); 856 $post = $parser->parseQueryString($query_string); 857 858 $files = array(); 859 foreach ($parts as $part) { 860 if ($part->isVariable()) { 861 continue; 862 } 863 864 $files[$part->getName()] = $part->getPHPFileDictionary(); 865 } 866 $_FILES = $files; 867 } else { 868 $post = $parser->parseQueryString($raw_input); 869 } 870 871 $_POST = $post; 872 PhabricatorStartup::rebuildRequest(); 873 } else if ($_POST) { 874 $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); 875 if (is_array($post)) { 876 $_POST = $post; 877 PhabricatorStartup::rebuildRequest(); 878 } 879 } 880 } 881 882}