mirror of
https://we.phorge.it/source/phorge.git
synced 2025-04-04 16:38:24 +02:00
Move lingering "Aphront" classes to Phabricator
Summary: Ref T13395. Moves some Aphront classes from libphutil to Phabricator. Test Plan: Grepped for symbols in libphutil and Arcanist. Maniphest Tasks: T13395 Differential Revision: https://secure.phabricator.com/D20975
This commit is contained in:
parent
2327578adc
commit
af84f215f9
10 changed files with 1037 additions and 0 deletions
|
@ -224,6 +224,8 @@ phutil_register_library_map(array(
|
||||||
'AphrontFormView' => 'view/form/AphrontFormView.php',
|
'AphrontFormView' => 'view/form/AphrontFormView.php',
|
||||||
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
|
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
|
||||||
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
|
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
|
||||||
|
'AphrontHTTPHeaderParser' => 'aphront/headerparser/AphrontHTTPHeaderParser.php',
|
||||||
|
'AphrontHTTPHeaderParserTestCase' => 'aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php',
|
||||||
'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
|
'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
|
||||||
'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
|
'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
|
||||||
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
|
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
|
||||||
|
@ -242,6 +244,9 @@ phutil_register_library_map(array(
|
||||||
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
|
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
|
||||||
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
|
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
|
||||||
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
|
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
|
||||||
|
'AphrontMultipartParser' => 'aphront/multipartparser/AphrontMultipartParser.php',
|
||||||
|
'AphrontMultipartParserTestCase' => 'aphront/multipartparser/__tests__/AphrontMultipartParserTestCase.php',
|
||||||
|
'AphrontMultipartPart' => 'aphront/multipartparser/AphrontMultipartPart.php',
|
||||||
'AphrontMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
|
'AphrontMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
|
||||||
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
|
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
|
||||||
'AphrontMySQLiDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
|
'AphrontMySQLiDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
|
||||||
|
@ -265,12 +270,14 @@ phutil_register_library_map(array(
|
||||||
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
|
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
|
||||||
'AphrontRequest' => 'aphront/AphrontRequest.php',
|
'AphrontRequest' => 'aphront/AphrontRequest.php',
|
||||||
'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
|
'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
|
||||||
|
'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php',
|
||||||
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
|
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
|
||||||
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
|
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
|
||||||
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
|
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
|
||||||
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
|
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
|
||||||
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
|
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
|
||||||
'AphrontSchemaQueryException' => 'infrastructure/storage/exception/AphrontSchemaQueryException.php',
|
'AphrontSchemaQueryException' => 'infrastructure/storage/exception/AphrontSchemaQueryException.php',
|
||||||
|
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
|
||||||
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
|
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
|
||||||
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
|
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
|
||||||
'AphrontSite' => 'aphront/site/AphrontSite.php',
|
'AphrontSite' => 'aphront/site/AphrontSite.php',
|
||||||
|
@ -286,6 +293,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php',
|
'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php',
|
||||||
'AphrontView' => 'view/AphrontView.php',
|
'AphrontView' => 'view/AphrontView.php',
|
||||||
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
|
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
|
||||||
|
'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
|
||||||
'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
|
'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
|
||||||
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
|
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
|
||||||
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
|
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
|
||||||
|
@ -6170,6 +6178,8 @@ phutil_register_library_map(array(
|
||||||
'AphrontFormView' => 'AphrontView',
|
'AphrontFormView' => 'AphrontView',
|
||||||
'AphrontGlyphBarView' => 'AphrontBarView',
|
'AphrontGlyphBarView' => 'AphrontBarView',
|
||||||
'AphrontHTMLResponse' => 'AphrontResponse',
|
'AphrontHTMLResponse' => 'AphrontResponse',
|
||||||
|
'AphrontHTTPHeaderParser' => 'Phobject',
|
||||||
|
'AphrontHTTPHeaderParserTestCase' => 'PhutilTestCase',
|
||||||
'AphrontHTTPParameterType' => 'Phobject',
|
'AphrontHTTPParameterType' => 'Phobject',
|
||||||
'AphrontHTTPProxyResponse' => 'AphrontResponse',
|
'AphrontHTTPProxyResponse' => 'AphrontResponse',
|
||||||
'AphrontHTTPSink' => 'Phobject',
|
'AphrontHTTPSink' => 'Phobject',
|
||||||
|
@ -6188,6 +6198,9 @@ phutil_register_library_map(array(
|
||||||
'AphrontMalformedRequestException' => 'AphrontException',
|
'AphrontMalformedRequestException' => 'AphrontException',
|
||||||
'AphrontMoreView' => 'AphrontView',
|
'AphrontMoreView' => 'AphrontView',
|
||||||
'AphrontMultiColumnView' => 'AphrontView',
|
'AphrontMultiColumnView' => 'AphrontView',
|
||||||
|
'AphrontMultipartParser' => 'Phobject',
|
||||||
|
'AphrontMultipartParserTestCase' => 'PhutilTestCase',
|
||||||
|
'AphrontMultipartPart' => 'Phobject',
|
||||||
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||||
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||||
|
@ -6214,11 +6227,13 @@ phutil_register_library_map(array(
|
||||||
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
||||||
'AphrontRequest' => 'Phobject',
|
'AphrontRequest' => 'Phobject',
|
||||||
'AphrontRequestExceptionHandler' => 'Phobject',
|
'AphrontRequestExceptionHandler' => 'Phobject',
|
||||||
|
'AphrontRequestStream' => 'Phobject',
|
||||||
'AphrontRequestTestCase' => 'PhabricatorTestCase',
|
'AphrontRequestTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontResponse' => 'Phobject',
|
'AphrontResponse' => 'Phobject',
|
||||||
'AphrontRoutingMap' => 'Phobject',
|
'AphrontRoutingMap' => 'Phobject',
|
||||||
'AphrontRoutingResult' => 'Phobject',
|
'AphrontRoutingResult' => 'Phobject',
|
||||||
'AphrontSchemaQueryException' => 'AphrontQueryException',
|
'AphrontSchemaQueryException' => 'AphrontQueryException',
|
||||||
|
'AphrontScopedUnguardedWriteCapability' => 'Phobject',
|
||||||
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
'AphrontSideNavFilterView' => 'AphrontView',
|
'AphrontSideNavFilterView' => 'AphrontView',
|
||||||
'AphrontSite' => 'Phobject',
|
'AphrontSite' => 'Phobject',
|
||||||
|
@ -6237,6 +6252,7 @@ phutil_register_library_map(array(
|
||||||
'PhutilSafeHTMLProducerInterface',
|
'PhutilSafeHTMLProducerInterface',
|
||||||
),
|
),
|
||||||
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
|
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
|
||||||
|
'AphrontWriteGuard' => 'Phobject',
|
||||||
'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
|
'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
|
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
|
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
|
||||||
|
|
150
src/aphront/headerparser/AphrontHTTPHeaderParser.php
Normal file
150
src/aphront/headerparser/AphrontHTTPHeaderParser.php
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontHTTPHeaderParser extends Phobject {
|
||||||
|
|
||||||
|
private $name;
|
||||||
|
private $content;
|
||||||
|
private $pairs;
|
||||||
|
|
||||||
|
public function parseRawHeader($raw_header) {
|
||||||
|
$this->name = null;
|
||||||
|
$this->content = null;
|
||||||
|
|
||||||
|
$parts = explode(':', $raw_header, 2);
|
||||||
|
$this->name = trim($parts[0]);
|
||||||
|
if (count($parts) > 1) {
|
||||||
|
$this->content = trim($parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pairs = null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaderName() {
|
||||||
|
$this->requireParse();
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaderContent() {
|
||||||
|
$this->requireParse();
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaderContentAsPairs() {
|
||||||
|
$content = $this->getHeaderContent();
|
||||||
|
|
||||||
|
|
||||||
|
$state = 'prekey';
|
||||||
|
$length = strlen($content);
|
||||||
|
|
||||||
|
$pair_name = null;
|
||||||
|
$pair_value = null;
|
||||||
|
|
||||||
|
$pairs = array();
|
||||||
|
$ii = 0;
|
||||||
|
while ($ii < $length) {
|
||||||
|
$c = $content[$ii];
|
||||||
|
|
||||||
|
switch ($state) {
|
||||||
|
case 'prekey';
|
||||||
|
// We're eating space in front of a key.
|
||||||
|
if ($c == ' ') {
|
||||||
|
$ii++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$pair_name = '';
|
||||||
|
$state = 'key';
|
||||||
|
break;
|
||||||
|
case 'key';
|
||||||
|
// We're parsing a key name until we find "=" or ";".
|
||||||
|
if ($c == ';') {
|
||||||
|
$state = 'done';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c == '=') {
|
||||||
|
$ii++;
|
||||||
|
$state = 'value';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ii++;
|
||||||
|
$pair_name .= $c;
|
||||||
|
break;
|
||||||
|
case 'value':
|
||||||
|
// We found an "=", so now figure out if the value is quoted
|
||||||
|
// or not.
|
||||||
|
if ($c == '"') {
|
||||||
|
$ii++;
|
||||||
|
$state = 'quoted';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$state = 'unquoted';
|
||||||
|
break;
|
||||||
|
case 'quoted':
|
||||||
|
// We're in a quoted string, parse until we find the closing quote.
|
||||||
|
if ($c == '"') {
|
||||||
|
$ii++;
|
||||||
|
$state = 'done';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ii++;
|
||||||
|
$pair_value .= $c;
|
||||||
|
break;
|
||||||
|
case 'unquoted':
|
||||||
|
// We're in an unquoted string, parse until we find a space or a
|
||||||
|
// semicolon.
|
||||||
|
if ($c == ' ' || $c == ';') {
|
||||||
|
$state = 'done';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$ii++;
|
||||||
|
$pair_value .= $c;
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
// We parsed something, so eat any trailing whitespace and semicolons
|
||||||
|
// and look for a new value.
|
||||||
|
if ($c == ' ' || $c == ';') {
|
||||||
|
$ii++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pairs[] = array(
|
||||||
|
$pair_name,
|
||||||
|
$pair_value,
|
||||||
|
);
|
||||||
|
|
||||||
|
$pair_name = null;
|
||||||
|
$pair_value = null;
|
||||||
|
|
||||||
|
$state = 'prekey';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state == 'quoted') {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Header has unterminated double quote for key "%s".',
|
||||||
|
$pair_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pair_name !== null) {
|
||||||
|
$pairs[] = array(
|
||||||
|
$pair_name,
|
||||||
|
$pair_value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requireParse() {
|
||||||
|
if ($this->name === null) {
|
||||||
|
throw new PhutilInvalidStateException('parseRawHeader');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontHTTPHeaderParserTestCase extends PhutilTestCase {
|
||||||
|
|
||||||
|
public function testHeaderParser() {
|
||||||
|
$cases = array(
|
||||||
|
array(
|
||||||
|
'Key: x; y; z',
|
||||||
|
'Key',
|
||||||
|
'x; y; z',
|
||||||
|
array(
|
||||||
|
array('x', null),
|
||||||
|
array('y', null),
|
||||||
|
array('z', null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Content-Disposition: form-data; name="label"',
|
||||||
|
'Content-Disposition',
|
||||||
|
'form-data; name="label"',
|
||||||
|
array(
|
||||||
|
array('form-data', null),
|
||||||
|
array('name', 'label'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Content-Type: multipart/form-data; charset=utf-8',
|
||||||
|
'Content-Type',
|
||||||
|
'multipart/form-data; charset=utf-8',
|
||||||
|
array(
|
||||||
|
array('multipart/form-data', null),
|
||||||
|
array('charset', 'utf-8'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Content-Type: application/octet-stream; charset="ut',
|
||||||
|
'Content-Type',
|
||||||
|
'application/octet-stream; charset="ut',
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Content-Type: multipart/form-data; boundary=ABCDEFG',
|
||||||
|
'Content-Type',
|
||||||
|
'multipart/form-data; boundary=ABCDEFG',
|
||||||
|
array(
|
||||||
|
array('multipart/form-data', null),
|
||||||
|
array('boundary', 'ABCDEFG'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Content-Type: multipart/form-data; boundary="ABCDEFG"',
|
||||||
|
'Content-Type',
|
||||||
|
'multipart/form-data; boundary="ABCDEFG"',
|
||||||
|
array(
|
||||||
|
array('multipart/form-data', null),
|
||||||
|
array('boundary', 'ABCDEFG'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($cases as $case) {
|
||||||
|
$input = $case[0];
|
||||||
|
$expect_name = $case[1];
|
||||||
|
$expect_content = $case[2];
|
||||||
|
|
||||||
|
$parser = id(new AphrontHTTPHeaderParser())
|
||||||
|
->parseRawHeader($input);
|
||||||
|
|
||||||
|
$actual_name = $parser->getHeaderName();
|
||||||
|
$actual_content = $parser->getHeaderContent();
|
||||||
|
|
||||||
|
$this->assertEqual(
|
||||||
|
$expect_name,
|
||||||
|
$actual_name,
|
||||||
|
pht('Header name for: %s', $input));
|
||||||
|
|
||||||
|
$this->assertEqual(
|
||||||
|
$expect_content,
|
||||||
|
$actual_content,
|
||||||
|
pht('Header content for: %s', $input));
|
||||||
|
|
||||||
|
if (isset($case[3])) {
|
||||||
|
$expect_pairs = $case[3];
|
||||||
|
|
||||||
|
$caught = null;
|
||||||
|
try {
|
||||||
|
$actual_pairs = $parser->getHeaderContentAsPairs();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$caught = $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expect_pairs === false) {
|
||||||
|
$this->assertEqual(
|
||||||
|
true,
|
||||||
|
($caught instanceof Exception),
|
||||||
|
pht('Expect exception for header pairs of: %s', $input));
|
||||||
|
} else {
|
||||||
|
$this->assertEqual(
|
||||||
|
$expect_pairs,
|
||||||
|
$actual_pairs,
|
||||||
|
pht('Header pairs for: %s', $input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
249
src/aphront/multipartparser/AphrontMultipartParser.php
Normal file
249
src/aphront/multipartparser/AphrontMultipartParser.php
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontMultipartParser extends Phobject {
|
||||||
|
|
||||||
|
private $contentType;
|
||||||
|
private $boundary;
|
||||||
|
|
||||||
|
private $buffer;
|
||||||
|
private $body;
|
||||||
|
private $state;
|
||||||
|
|
||||||
|
private $part;
|
||||||
|
private $parts;
|
||||||
|
|
||||||
|
public function setContentType($content_type) {
|
||||||
|
$this->contentType = $content_type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContentType() {
|
||||||
|
return $this->contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beginParse() {
|
||||||
|
$content_type = $this->getContentType();
|
||||||
|
if ($content_type === null) {
|
||||||
|
throw new PhutilInvalidStateException('setContentType');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('(^multipart/form-data)', $content_type)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Expected "multipart/form-data" content type when executing a '.
|
||||||
|
'multipart body read.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$type_parts = preg_split('(\s*;\s*)', $content_type);
|
||||||
|
$boundary = null;
|
||||||
|
foreach ($type_parts as $type_part) {
|
||||||
|
$matches = null;
|
||||||
|
if (preg_match('(^boundary=(.*))', $type_part, $matches)) {
|
||||||
|
$boundary = $matches[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($boundary === null) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Received "multipart/form-data" request with no "boundary".'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->parts = array();
|
||||||
|
$this->part = null;
|
||||||
|
|
||||||
|
$this->buffer = '';
|
||||||
|
$this->boundary = $boundary;
|
||||||
|
|
||||||
|
// We're looking for a (usually empty) body before the first boundary.
|
||||||
|
$this->state = 'bodynewline';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function continueParse($bytes) {
|
||||||
|
$this->buffer .= $bytes;
|
||||||
|
|
||||||
|
$continue = true;
|
||||||
|
while ($continue) {
|
||||||
|
switch ($this->state) {
|
||||||
|
case 'endboundary':
|
||||||
|
// We've just parsed a boundary. Next, we expect either "--" (which
|
||||||
|
// indicates we've reached the end of the parts) or "\r\n" (which
|
||||||
|
// indicates we should read the headers for the next part).
|
||||||
|
|
||||||
|
if (strlen($this->buffer) < 2) {
|
||||||
|
// We don't have enough bytes yet, so wait for more.
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strncmp($this->buffer, '--', 2)) {
|
||||||
|
// This is "--" after a boundary, so we're done. We'll read the
|
||||||
|
// rest of the body (the "epilogue") and discard it.
|
||||||
|
$this->buffer = substr($this->buffer, 2);
|
||||||
|
$this->state = 'epilogue';
|
||||||
|
|
||||||
|
$this->part = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strncmp($this->buffer, "\r\n", 2)) {
|
||||||
|
// This is "\r\n" after a boundary, so we're going to going to
|
||||||
|
// read the headers for a part.
|
||||||
|
$this->buffer = substr($this->buffer, 2);
|
||||||
|
$this->state = 'header';
|
||||||
|
|
||||||
|
// Create the object to hold the part we're about to read.
|
||||||
|
$part = new AphrontMultipartPart();
|
||||||
|
$this->parts[] = $part;
|
||||||
|
$this->part = $part;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception(
|
||||||
|
pht('Expected "\r\n" or "--" after multipart data boundary.'));
|
||||||
|
case 'header':
|
||||||
|
// We've just parsed a boundary, followed by "\r\n". We are going
|
||||||
|
// to read the headers for this part. They are in the form of HTTP
|
||||||
|
// headers and terminated by "\r\n". The section is terminated by
|
||||||
|
// a line with no header on it.
|
||||||
|
|
||||||
|
if (strlen($this->buffer) < 2) {
|
||||||
|
// We don't have enough data to find a "\r\n", so wait for more.
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strncmp("\r\n", $this->buffer, 2)) {
|
||||||
|
// This line immediately began "\r\n", so we're done with parsing
|
||||||
|
// headers. Start parsing the body.
|
||||||
|
$this->buffer = substr($this->buffer, 2);
|
||||||
|
$this->state = 'body';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is an actual header, so look for the end of it.
|
||||||
|
$header_len = strpos($this->buffer, "\r\n");
|
||||||
|
if ($header_len === false) {
|
||||||
|
// We don't have a full header yet, so wait for more data.
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header_buf = substr($this->buffer, 0, $header_len);
|
||||||
|
$this->part->appendRawHeader($header_buf);
|
||||||
|
|
||||||
|
$this->buffer = substr($this->buffer, $header_len + 2);
|
||||||
|
break;
|
||||||
|
case 'body':
|
||||||
|
// We've parsed a boundary and headers, and are parsing the data for
|
||||||
|
// this part. The data is terminated by "\r\n--", then the boundary.
|
||||||
|
|
||||||
|
// We'll look for "\r\n", then switch to the "bodynewline" state if
|
||||||
|
// we find it.
|
||||||
|
|
||||||
|
$marker = "\r";
|
||||||
|
$marker_pos = strpos($this->buffer, $marker);
|
||||||
|
|
||||||
|
if ($marker_pos === false) {
|
||||||
|
// There's no "\r" anywhere in the buffer, so we can just read it
|
||||||
|
// as provided. Then, since we read all the data, we're done until
|
||||||
|
// we get more.
|
||||||
|
|
||||||
|
// Note that if we're in the preamble, we won't have a "part"
|
||||||
|
// object and will just discard the data.
|
||||||
|
if ($this->part) {
|
||||||
|
$this->part->appendData($this->buffer);
|
||||||
|
}
|
||||||
|
$this->buffer = '';
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($marker_pos > 0) {
|
||||||
|
// If there are bytes before the "\r",
|
||||||
|
if ($this->part) {
|
||||||
|
$this->part->appendData(substr($this->buffer, 0, $marker_pos));
|
||||||
|
}
|
||||||
|
$this->buffer = substr($this->buffer, $marker_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expect = "\r\n";
|
||||||
|
$expect_len = strlen($expect);
|
||||||
|
if (strlen($this->buffer) < $expect_len) {
|
||||||
|
// We don't have enough bytes yet to know if this is "\r\n"
|
||||||
|
// or not.
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncmp($this->buffer, $expect, $expect_len)) {
|
||||||
|
// The next two bytes aren't "\r\n", so eat them and go looking
|
||||||
|
// for more newlines.
|
||||||
|
if ($this->part) {
|
||||||
|
$this->part->appendData(substr($this->buffer, 0, $expect_len));
|
||||||
|
}
|
||||||
|
$this->buffer = substr($this->buffer, $expect_len);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eat the "\r\n".
|
||||||
|
$this->buffer = substr($this->buffer, $expect_len);
|
||||||
|
$this->state = 'bodynewline';
|
||||||
|
break;
|
||||||
|
case 'bodynewline':
|
||||||
|
// We've parsed a newline in a body, or we just started parsing the
|
||||||
|
// request. In either case, we're looking for "--", then the boundary.
|
||||||
|
// If we find it, this section is done. If we don't, we consume the
|
||||||
|
// bytes and move on.
|
||||||
|
|
||||||
|
$expect = '--'.$this->boundary;
|
||||||
|
$expect_len = strlen($expect);
|
||||||
|
|
||||||
|
if (strlen($this->buffer) < $expect_len) {
|
||||||
|
// We don't have enough bytes yet, so wait for more.
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncmp($this->buffer, $expect, $expect_len)) {
|
||||||
|
// This wasn't the boundary, so return to the "body" state and
|
||||||
|
// consume it. (But first, we need to append the "\r\n" which we
|
||||||
|
// ate earlier.)
|
||||||
|
if ($this->part) {
|
||||||
|
$this->part->appendData("\r\n");
|
||||||
|
}
|
||||||
|
$this->state = 'body';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the boundary, so toss it and move on.
|
||||||
|
$this->buffer = substr($this->buffer, $expect_len);
|
||||||
|
$this->state = 'endboundary';
|
||||||
|
break;
|
||||||
|
case 'epilogue':
|
||||||
|
// We just discard any epilogue.
|
||||||
|
$this->buffer = '';
|
||||||
|
$continue = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unknown parser state "%s".\n',
|
||||||
|
$this->state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function endParse() {
|
||||||
|
if ($this->state !== 'epilogue') {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Expected "multipart/form-data" parse to end '.
|
||||||
|
'in state "epilogue".'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
96
src/aphront/multipartparser/AphrontMultipartPart.php
Normal file
96
src/aphront/multipartparser/AphrontMultipartPart.php
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontMultipartPart extends Phobject {
|
||||||
|
|
||||||
|
private $headers = array();
|
||||||
|
private $value = '';
|
||||||
|
|
||||||
|
private $name;
|
||||||
|
private $filename;
|
||||||
|
private $tempFile;
|
||||||
|
private $byteSize = 0;
|
||||||
|
|
||||||
|
public function appendRawHeader($bytes) {
|
||||||
|
$parser = id(new AphrontHTTPHeaderParser())
|
||||||
|
->parseRawHeader($bytes);
|
||||||
|
|
||||||
|
$header_name = $parser->getHeaderName();
|
||||||
|
|
||||||
|
$this->headers[] = array(
|
||||||
|
$header_name,
|
||||||
|
$parser->getHeaderContent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (strtolower($header_name) === 'content-disposition') {
|
||||||
|
$pairs = $parser->getHeaderContentAsPairs();
|
||||||
|
foreach ($pairs as $pair) {
|
||||||
|
list($key, $value) = $pair;
|
||||||
|
switch ($key) {
|
||||||
|
case 'filename':
|
||||||
|
$this->filename = $value;
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
$this->name = $value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function appendData($bytes) {
|
||||||
|
$this->byteSize += strlen($bytes);
|
||||||
|
|
||||||
|
if ($this->isVariable()) {
|
||||||
|
$this->value .= $bytes;
|
||||||
|
} else {
|
||||||
|
if (!$this->tempFile) {
|
||||||
|
$this->tempFile = new TempFile(getmypid().'.upload');
|
||||||
|
}
|
||||||
|
Filesystem::appendFile($this->tempFile, $bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isVariable() {
|
||||||
|
return ($this->filename === null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVariableValue() {
|
||||||
|
if (!$this->isVariable()) {
|
||||||
|
throw new Exception(pht('This part is not a variable!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPHPFileDictionary() {
|
||||||
|
if (!$this->tempFile) {
|
||||||
|
$this->appendData('');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime_type = 'application/octet-stream';
|
||||||
|
foreach ($this->headers as $header) {
|
||||||
|
list($name, $value) = $header;
|
||||||
|
if (strtolower($name) == 'content-type') {
|
||||||
|
$mime_type = $value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'name' => $this->filename,
|
||||||
|
'type' => $mime_type,
|
||||||
|
'tmp_name' => (string)$this->tempFile,
|
||||||
|
'error' => 0,
|
||||||
|
'size' => $this->byteSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontMultipartParserTestCase extends PhutilTestCase {
|
||||||
|
|
||||||
|
public function testParser() {
|
||||||
|
$map = array(
|
||||||
|
array(
|
||||||
|
'data' => 'simple.txt',
|
||||||
|
'variables' => array(
|
||||||
|
array('a', 'b'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$data_dir = dirname(__FILE__).'/data/';
|
||||||
|
foreach ($map as $test_case) {
|
||||||
|
$data = Filesystem::readFile($data_dir.$test_case['data']);
|
||||||
|
$data = str_replace("\n", "\r\n", $data);
|
||||||
|
|
||||||
|
$parser = id(new AphrontMultipartParser())
|
||||||
|
->setContentType('multipart/form-data; boundary=ABCDEFG');
|
||||||
|
$parser->beginParse();
|
||||||
|
$parser->continueParse($data);
|
||||||
|
$parts = $parser->endParse();
|
||||||
|
|
||||||
|
$variables = array();
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (!$part->isVariable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables[] = array(
|
||||||
|
$part->getName(),
|
||||||
|
$part->getVariableValue(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expect_variables = idx($test_case, 'variables', array());
|
||||||
|
$this->assertEqual($expect_variables, $variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
5
src/aphront/multipartparser/__tests__/data/simple.txt
Normal file
5
src/aphront/multipartparser/__tests__/data/simple.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
--ABCDEFG
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--ABCDEFG--
|
92
src/aphront/requeststream/AphrontRequestStream.php
Normal file
92
src/aphront/requeststream/AphrontRequestStream.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontRequestStream extends Phobject {
|
||||||
|
|
||||||
|
private $encoding;
|
||||||
|
private $stream;
|
||||||
|
private $closed;
|
||||||
|
private $iterator;
|
||||||
|
|
||||||
|
public function setEncoding($encoding) {
|
||||||
|
$this->encoding = $encoding;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEncoding() {
|
||||||
|
return $this->encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator() {
|
||||||
|
if (!$this->iterator) {
|
||||||
|
$this->iterator = new PhutilStreamIterator($this->getStream());
|
||||||
|
}
|
||||||
|
return $this->iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readData() {
|
||||||
|
if (!$this->iterator) {
|
||||||
|
$iterator = $this->getIterator();
|
||||||
|
$iterator->rewind();
|
||||||
|
} else {
|
||||||
|
$iterator = $this->getIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$iterator->valid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $iterator->current();
|
||||||
|
$iterator->next();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStream() {
|
||||||
|
if (!$this->stream) {
|
||||||
|
$this->stream = $this->newStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function newStream() {
|
||||||
|
$stream = fopen('php://input', 'rb');
|
||||||
|
if (!$stream) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Failed to open stream "%s" for reading.',
|
||||||
|
'php://input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoding = $this->getEncoding();
|
||||||
|
if ($encoding === 'gzip') {
|
||||||
|
// This parameter is magic. Values 0-15 express a time/memory tradeoff,
|
||||||
|
// but the largest value (15) corresponds to only 32KB of memory and
|
||||||
|
// data encoded with a smaller window size than the one we pass can not
|
||||||
|
// be decompressed. Always pass the maximum window size.
|
||||||
|
|
||||||
|
// Additionally, you can add 16 (to enable gzip) or 32 (to enable both
|
||||||
|
// gzip and zlib). Add 32 to support both.
|
||||||
|
$zlib_window = 15 + 32;
|
||||||
|
|
||||||
|
$ok = stream_filter_append(
|
||||||
|
$stream,
|
||||||
|
'zlib.inflate',
|
||||||
|
STREAM_FILTER_READ,
|
||||||
|
array(
|
||||||
|
'window' => $zlib_window,
|
||||||
|
));
|
||||||
|
if (!$ok) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Failed to append filter "%s" to input stream while processing '.
|
||||||
|
'a request with "%s" encoding.',
|
||||||
|
'zlib.inflate',
|
||||||
|
$encoding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontScopedUnguardedWriteCapability extends Phobject {
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
AphrontWriteGuard::endUnguardedWrites();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
267
src/aphront/writeguard/AphrontWriteGuard.php
Normal file
267
src/aphront/writeguard/AphrontWriteGuard.php
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard writes against CSRF. The Aphront structure takes care of most of this
|
||||||
|
* for you, you just need to call:
|
||||||
|
*
|
||||||
|
* AphrontWriteGuard::willWrite();
|
||||||
|
*
|
||||||
|
* ...before executing a write against any new kind of storage engine. MySQL
|
||||||
|
* databases and the default file storage engines are already covered, but if
|
||||||
|
* you introduce new types of datastores make sure their writes are guarded. If
|
||||||
|
* you don't guard writes and make a mistake doing CSRF checks in a controller,
|
||||||
|
* a CSRF vulnerability can escape undetected.
|
||||||
|
*
|
||||||
|
* If you need to execute writes on a page which doesn't have CSRF tokens (for
|
||||||
|
* example, because you need to do logging), you can temporarily disable the
|
||||||
|
* write guard by calling:
|
||||||
|
*
|
||||||
|
* AphrontWriteGuard::beginUnguardedWrites();
|
||||||
|
* do_logging_write();
|
||||||
|
* AphrontWriteGuard::endUnguardedWrites();
|
||||||
|
*
|
||||||
|
* This is dangerous, because it disables the backup layer of CSRF protection
|
||||||
|
* this class provides. You should need this only very, very rarely.
|
||||||
|
*
|
||||||
|
* @task protect Protecting Writes
|
||||||
|
* @task disable Disabling Protection
|
||||||
|
* @task manage Managing Write Guards
|
||||||
|
* @task internal Internals
|
||||||
|
*/
|
||||||
|
final class AphrontWriteGuard extends Phobject {
|
||||||
|
|
||||||
|
private static $instance;
|
||||||
|
private static $allowUnguardedWrites = false;
|
||||||
|
|
||||||
|
private $callback;
|
||||||
|
private $allowDepth = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Managing Write Guards )---------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new write guard for a request. Only one write guard may be
|
||||||
|
* active at a time. You must explicitly call @{method:dispose} when you are
|
||||||
|
* done with a write guard:
|
||||||
|
*
|
||||||
|
* $guard = new AphrontWriteGuard($callback);
|
||||||
|
* // ...
|
||||||
|
* $guard->dispose();
|
||||||
|
*
|
||||||
|
* Normally, you do not need to manage guards yourself -- the Aphront stack
|
||||||
|
* handles it for you.
|
||||||
|
*
|
||||||
|
* This class accepts a callback, which will be invoked when a write is
|
||||||
|
* attempted. The callback should validate the presence of a CSRF token in
|
||||||
|
* the request, or abort the request (e.g., by throwing an exception) if a
|
||||||
|
* valid token isn't present.
|
||||||
|
*
|
||||||
|
* @param callable CSRF callback.
|
||||||
|
* @return this
|
||||||
|
* @task manage
|
||||||
|
*/
|
||||||
|
public function __construct($callback) {
|
||||||
|
if (self::$instance) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'An %s already exists. Dispose of the previous guard '.
|
||||||
|
'before creating a new one.',
|
||||||
|
__CLASS__));
|
||||||
|
}
|
||||||
|
if (self::$allowUnguardedWrites) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'An %s is being created in a context which permits '.
|
||||||
|
'unguarded writes unconditionally. This is not allowed and '.
|
||||||
|
'indicates a serious error.',
|
||||||
|
__CLASS__));
|
||||||
|
}
|
||||||
|
$this->callback = $callback;
|
||||||
|
self::$instance = $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of the active write guard. You must call this method when you are
|
||||||
|
* done with a write guard. You do not normally need to call this yourself.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @task manage
|
||||||
|
*/
|
||||||
|
public function dispose() {
|
||||||
|
if (!self::$instance) {
|
||||||
|
throw new Exception(pht(
|
||||||
|
'Attempting to dispose of write guard, but no write guard is active!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->allowDepth > 0) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Imbalanced %s: more %s calls than %s calls.',
|
||||||
|
__CLASS__,
|
||||||
|
'beginUnguardedWrites()',
|
||||||
|
'endUnguardedWrites()'));
|
||||||
|
}
|
||||||
|
self::$instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if there is an active write guard.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* @task manage
|
||||||
|
*/
|
||||||
|
public static function isGuardActive() {
|
||||||
|
return (bool)self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return on instance of AphrontWriteGuard if it's active, or null
|
||||||
|
*
|
||||||
|
* @return AphrontWriteGuard|null
|
||||||
|
*/
|
||||||
|
public static function getInstance() {
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Protecting Writes )-------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare intention to perform a write, validating that writes are allowed.
|
||||||
|
* You should call this method before executing a write whenever you implement
|
||||||
|
* a new storage engine where information can be permanently kept.
|
||||||
|
*
|
||||||
|
* Writes are permitted if:
|
||||||
|
*
|
||||||
|
* - The request has valid CSRF tokens.
|
||||||
|
* - Unguarded writes have been temporarily enabled by a call to
|
||||||
|
* @{method:beginUnguardedWrites}.
|
||||||
|
* - All write guarding has been disabled with
|
||||||
|
* @{method:allowDangerousUnguardedWrites}.
|
||||||
|
*
|
||||||
|
* If none of these conditions are true, this method will throw and prevent
|
||||||
|
* the write.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @task protect
|
||||||
|
*/
|
||||||
|
public static function willWrite() {
|
||||||
|
if (!self::$instance) {
|
||||||
|
if (!self::$allowUnguardedWrites) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unguarded write! There must be an active %s to perform writes.',
|
||||||
|
__CLASS__));
|
||||||
|
} else {
|
||||||
|
// Unguarded writes are being allowed unconditionally.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = self::$instance;
|
||||||
|
if ($instance->allowDepth == 0) {
|
||||||
|
call_user_func($instance->callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Disabling Write Protection )----------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter a scope which permits unguarded writes. This works like
|
||||||
|
* @{method:beginUnguardedWrites} but returns an object which will end
|
||||||
|
* the unguarded write scope when its __destruct() method is called. This
|
||||||
|
* is useful to more easily handle exceptions correctly in unguarded write
|
||||||
|
* blocks:
|
||||||
|
*
|
||||||
|
* // Restores the guard even if do_logging() throws.
|
||||||
|
* function unguarded_scope() {
|
||||||
|
* $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||||
|
* do_logging();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @return AphrontScopedUnguardedWriteCapability Object which ends unguarded
|
||||||
|
* writes when it leaves scope.
|
||||||
|
* @task disable
|
||||||
|
*/
|
||||||
|
public static function beginScopedUnguardedWrites() {
|
||||||
|
self::beginUnguardedWrites();
|
||||||
|
return new AphrontScopedUnguardedWriteCapability();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin a block which permits unguarded writes. You should use this very
|
||||||
|
* sparingly, and only for things like logging where CSRF is not a concern.
|
||||||
|
*
|
||||||
|
* You must pair every call to @{method:beginUnguardedWrites} with a call to
|
||||||
|
* @{method:endUnguardedWrites}:
|
||||||
|
*
|
||||||
|
* AphrontWriteGuard::beginUnguardedWrites();
|
||||||
|
* do_logging();
|
||||||
|
* AphrontWriteGuard::endUnguardedWrites();
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @task disable
|
||||||
|
*/
|
||||||
|
public static function beginUnguardedWrites() {
|
||||||
|
if (!self::$instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$instance->allowDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare that you have finished performing unguarded writes. You must
|
||||||
|
* call this exactly once for each call to @{method:beginUnguardedWrites}.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @task disable
|
||||||
|
*/
|
||||||
|
public static function endUnguardedWrites() {
|
||||||
|
if (!self::$instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self::$instance->allowDepth <= 0) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Imbalanced %s: more %s calls than %s calls.',
|
||||||
|
__CLASS__,
|
||||||
|
'endUnguardedWrites()',
|
||||||
|
'beginUnguardedWrites()'));
|
||||||
|
}
|
||||||
|
self::$instance->allowDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow execution of unguarded writes. This is ONLY appropriate for use in
|
||||||
|
* script contexts or other contexts where you are guaranteed to never be
|
||||||
|
* vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS
|
||||||
|
* if you do not understand the consequences.
|
||||||
|
*
|
||||||
|
* If you need to perform unguarded writes on an otherwise guarded workflow
|
||||||
|
* which is vulnerable to CSRF, use @{method:beginUnguardedWrites}.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @task disable
|
||||||
|
*/
|
||||||
|
public static function allowDangerousUnguardedWrites($allow) {
|
||||||
|
if (self::$instance) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'You can not unconditionally disable %s by calling %s while a write '.
|
||||||
|
'guard is active. Use %s to temporarily allow unguarded writes.',
|
||||||
|
__CLASS__,
|
||||||
|
__FUNCTION__.'()',
|
||||||
|
'beginUnguardedWrites()'));
|
||||||
|
}
|
||||||
|
self::$allowUnguardedWrites = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue