@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
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}