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

Configure Feed

Select the types of activity you want to include in your feed.

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

+1037
+16
src/__phutil_library_map__.php
··· 224 224 'AphrontFormView' => 'view/form/AphrontFormView.php', 225 225 'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php', 226 226 'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php', 227 + 'AphrontHTTPHeaderParser' => 'aphront/headerparser/AphrontHTTPHeaderParser.php', 228 + 'AphrontHTTPHeaderParserTestCase' => 'aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php', 227 229 'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php', 228 230 'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php', 229 231 'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php', ··· 242 244 'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php', 243 245 'AphrontMoreView' => 'view/layout/AphrontMoreView.php', 244 246 'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php', 247 + 'AphrontMultipartParser' => 'aphront/multipartparser/AphrontMultipartParser.php', 248 + 'AphrontMultipartParserTestCase' => 'aphront/multipartparser/__tests__/AphrontMultipartParserTestCase.php', 249 + 'AphrontMultipartPart' => 'aphront/multipartparser/AphrontMultipartPart.php', 245 250 'AphrontMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLDatabaseConnection.php', 246 251 'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php', 247 252 'AphrontMySQLiDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php', ··· 265 270 'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php', 266 271 'AphrontRequest' => 'aphront/AphrontRequest.php', 267 272 'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php', 273 + 'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php', 268 274 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 269 275 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 270 276 'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php', 271 277 'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php', 272 278 'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php', 273 279 'AphrontSchemaQueryException' => 'infrastructure/storage/exception/AphrontSchemaQueryException.php', 280 + 'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php', 274 281 'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php', 275 282 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 276 283 'AphrontSite' => 'aphront/site/AphrontSite.php', ··· 286 293 'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php', 287 294 'AphrontView' => 'view/AphrontView.php', 288 295 'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', 296 + 'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php', 289 297 'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php', 290 298 'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php', 291 299 'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php', ··· 6170 6178 'AphrontFormView' => 'AphrontView', 6171 6179 'AphrontGlyphBarView' => 'AphrontBarView', 6172 6180 'AphrontHTMLResponse' => 'AphrontResponse', 6181 + 'AphrontHTTPHeaderParser' => 'Phobject', 6182 + 'AphrontHTTPHeaderParserTestCase' => 'PhutilTestCase', 6173 6183 'AphrontHTTPParameterType' => 'Phobject', 6174 6184 'AphrontHTTPProxyResponse' => 'AphrontResponse', 6175 6185 'AphrontHTTPSink' => 'Phobject', ··· 6188 6198 'AphrontMalformedRequestException' => 'AphrontException', 6189 6199 'AphrontMoreView' => 'AphrontView', 6190 6200 'AphrontMultiColumnView' => 'AphrontView', 6201 + 'AphrontMultipartParser' => 'Phobject', 6202 + 'AphrontMultipartParserTestCase' => 'PhutilTestCase', 6203 + 'AphrontMultipartPart' => 'Phobject', 6191 6204 'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 6192 6205 'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase', 6193 6206 'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', ··· 6214 6227 'AphrontReloadResponse' => 'AphrontRedirectResponse', 6215 6228 'AphrontRequest' => 'Phobject', 6216 6229 'AphrontRequestExceptionHandler' => 'Phobject', 6230 + 'AphrontRequestStream' => 'Phobject', 6217 6231 'AphrontRequestTestCase' => 'PhabricatorTestCase', 6218 6232 'AphrontResponse' => 'Phobject', 6219 6233 'AphrontRoutingMap' => 'Phobject', 6220 6234 'AphrontRoutingResult' => 'Phobject', 6221 6235 'AphrontSchemaQueryException' => 'AphrontQueryException', 6236 + 'AphrontScopedUnguardedWriteCapability' => 'Phobject', 6222 6237 'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType', 6223 6238 'AphrontSideNavFilterView' => 'AphrontView', 6224 6239 'AphrontSite' => 'Phobject', ··· 6237 6252 'PhutilSafeHTMLProducerInterface', 6238 6253 ), 6239 6254 'AphrontWebpageResponse' => 'AphrontHTMLResponse', 6255 + 'AphrontWriteGuard' => 'Phobject', 6240 6256 'ArcanistConduitAPIMethod' => 'ConduitAPIMethod', 6241 6257 'AuditConduitAPIMethod' => 'ConduitAPIMethod', 6242 6258 'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
+150
src/aphront/headerparser/AphrontHTTPHeaderParser.php
··· 1 + <?php 2 + 3 + final class AphrontHTTPHeaderParser extends Phobject { 4 + 5 + private $name; 6 + private $content; 7 + private $pairs; 8 + 9 + public function parseRawHeader($raw_header) { 10 + $this->name = null; 11 + $this->content = null; 12 + 13 + $parts = explode(':', $raw_header, 2); 14 + $this->name = trim($parts[0]); 15 + if (count($parts) > 1) { 16 + $this->content = trim($parts[1]); 17 + } 18 + 19 + $this->pairs = null; 20 + 21 + return $this; 22 + } 23 + 24 + public function getHeaderName() { 25 + $this->requireParse(); 26 + return $this->name; 27 + } 28 + 29 + public function getHeaderContent() { 30 + $this->requireParse(); 31 + return $this->content; 32 + } 33 + 34 + public function getHeaderContentAsPairs() { 35 + $content = $this->getHeaderContent(); 36 + 37 + 38 + $state = 'prekey'; 39 + $length = strlen($content); 40 + 41 + $pair_name = null; 42 + $pair_value = null; 43 + 44 + $pairs = array(); 45 + $ii = 0; 46 + while ($ii < $length) { 47 + $c = $content[$ii]; 48 + 49 + switch ($state) { 50 + case 'prekey'; 51 + // We're eating space in front of a key. 52 + if ($c == ' ') { 53 + $ii++; 54 + break; 55 + } 56 + $pair_name = ''; 57 + $state = 'key'; 58 + break; 59 + case 'key'; 60 + // We're parsing a key name until we find "=" or ";". 61 + if ($c == ';') { 62 + $state = 'done'; 63 + break; 64 + } 65 + 66 + if ($c == '=') { 67 + $ii++; 68 + $state = 'value'; 69 + break; 70 + } 71 + 72 + $ii++; 73 + $pair_name .= $c; 74 + break; 75 + case 'value': 76 + // We found an "=", so now figure out if the value is quoted 77 + // or not. 78 + if ($c == '"') { 79 + $ii++; 80 + $state = 'quoted'; 81 + break; 82 + } 83 + $state = 'unquoted'; 84 + break; 85 + case 'quoted': 86 + // We're in a quoted string, parse until we find the closing quote. 87 + if ($c == '"') { 88 + $ii++; 89 + $state = 'done'; 90 + break; 91 + } 92 + 93 + $ii++; 94 + $pair_value .= $c; 95 + break; 96 + case 'unquoted': 97 + // We're in an unquoted string, parse until we find a space or a 98 + // semicolon. 99 + if ($c == ' ' || $c == ';') { 100 + $state = 'done'; 101 + break; 102 + } 103 + $ii++; 104 + $pair_value .= $c; 105 + break; 106 + case 'done': 107 + // We parsed something, so eat any trailing whitespace and semicolons 108 + // and look for a new value. 109 + if ($c == ' ' || $c == ';') { 110 + $ii++; 111 + break; 112 + } 113 + 114 + $pairs[] = array( 115 + $pair_name, 116 + $pair_value, 117 + ); 118 + 119 + $pair_name = null; 120 + $pair_value = null; 121 + 122 + $state = 'prekey'; 123 + break; 124 + } 125 + } 126 + 127 + if ($state == 'quoted') { 128 + throw new Exception( 129 + pht( 130 + 'Header has unterminated double quote for key "%s".', 131 + $pair_name)); 132 + } 133 + 134 + if ($pair_name !== null) { 135 + $pairs[] = array( 136 + $pair_name, 137 + $pair_value, 138 + ); 139 + } 140 + 141 + return $pairs; 142 + } 143 + 144 + private function requireParse() { 145 + if ($this->name === null) { 146 + throw new PhutilInvalidStateException('parseRawHeader'); 147 + } 148 + } 149 + 150 + }
+108
src/aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php
··· 1 + <?php 2 + 3 + final class AphrontHTTPHeaderParserTestCase extends PhutilTestCase { 4 + 5 + public function testHeaderParser() { 6 + $cases = array( 7 + array( 8 + 'Key: x; y; z', 9 + 'Key', 10 + 'x; y; z', 11 + array( 12 + array('x', null), 13 + array('y', null), 14 + array('z', null), 15 + ), 16 + ), 17 + array( 18 + 'Content-Disposition: form-data; name="label"', 19 + 'Content-Disposition', 20 + 'form-data; name="label"', 21 + array( 22 + array('form-data', null), 23 + array('name', 'label'), 24 + ), 25 + ), 26 + array( 27 + 'Content-Type: multipart/form-data; charset=utf-8', 28 + 'Content-Type', 29 + 'multipart/form-data; charset=utf-8', 30 + array( 31 + array('multipart/form-data', null), 32 + array('charset', 'utf-8'), 33 + ), 34 + ), 35 + array( 36 + 'Content-Type: application/octet-stream; charset="ut', 37 + 'Content-Type', 38 + 'application/octet-stream; charset="ut', 39 + false, 40 + ), 41 + array( 42 + 'Content-Type: multipart/form-data; boundary=ABCDEFG', 43 + 'Content-Type', 44 + 'multipart/form-data; boundary=ABCDEFG', 45 + array( 46 + array('multipart/form-data', null), 47 + array('boundary', 'ABCDEFG'), 48 + ), 49 + ), 50 + array( 51 + 'Content-Type: multipart/form-data; boundary="ABCDEFG"', 52 + 'Content-Type', 53 + 'multipart/form-data; boundary="ABCDEFG"', 54 + array( 55 + array('multipart/form-data', null), 56 + array('boundary', 'ABCDEFG'), 57 + ), 58 + ), 59 + ); 60 + 61 + foreach ($cases as $case) { 62 + $input = $case[0]; 63 + $expect_name = $case[1]; 64 + $expect_content = $case[2]; 65 + 66 + $parser = id(new AphrontHTTPHeaderParser()) 67 + ->parseRawHeader($input); 68 + 69 + $actual_name = $parser->getHeaderName(); 70 + $actual_content = $parser->getHeaderContent(); 71 + 72 + $this->assertEqual( 73 + $expect_name, 74 + $actual_name, 75 + pht('Header name for: %s', $input)); 76 + 77 + $this->assertEqual( 78 + $expect_content, 79 + $actual_content, 80 + pht('Header content for: %s', $input)); 81 + 82 + if (isset($case[3])) { 83 + $expect_pairs = $case[3]; 84 + 85 + $caught = null; 86 + try { 87 + $actual_pairs = $parser->getHeaderContentAsPairs(); 88 + } catch (Exception $ex) { 89 + $caught = $ex; 90 + } 91 + 92 + if ($expect_pairs === false) { 93 + $this->assertEqual( 94 + true, 95 + ($caught instanceof Exception), 96 + pht('Expect exception for header pairs of: %s', $input)); 97 + } else { 98 + $this->assertEqual( 99 + $expect_pairs, 100 + $actual_pairs, 101 + pht('Header pairs for: %s', $input)); 102 + } 103 + } 104 + } 105 + } 106 + 107 + 108 + }
+249
src/aphront/multipartparser/AphrontMultipartParser.php
··· 1 + <?php 2 + 3 + final class AphrontMultipartParser extends Phobject { 4 + 5 + private $contentType; 6 + private $boundary; 7 + 8 + private $buffer; 9 + private $body; 10 + private $state; 11 + 12 + private $part; 13 + private $parts; 14 + 15 + public function setContentType($content_type) { 16 + $this->contentType = $content_type; 17 + return $this; 18 + } 19 + 20 + public function getContentType() { 21 + return $this->contentType; 22 + } 23 + 24 + public function beginParse() { 25 + $content_type = $this->getContentType(); 26 + if ($content_type === null) { 27 + throw new PhutilInvalidStateException('setContentType'); 28 + } 29 + 30 + if (!preg_match('(^multipart/form-data)', $content_type)) { 31 + throw new Exception( 32 + pht( 33 + 'Expected "multipart/form-data" content type when executing a '. 34 + 'multipart body read.')); 35 + } 36 + 37 + $type_parts = preg_split('(\s*;\s*)', $content_type); 38 + $boundary = null; 39 + foreach ($type_parts as $type_part) { 40 + $matches = null; 41 + if (preg_match('(^boundary=(.*))', $type_part, $matches)) { 42 + $boundary = $matches[1]; 43 + break; 44 + } 45 + } 46 + 47 + if ($boundary === null) { 48 + throw new Exception( 49 + pht('Received "multipart/form-data" request with no "boundary".')); 50 + } 51 + 52 + $this->parts = array(); 53 + $this->part = null; 54 + 55 + $this->buffer = ''; 56 + $this->boundary = $boundary; 57 + 58 + // We're looking for a (usually empty) body before the first boundary. 59 + $this->state = 'bodynewline'; 60 + } 61 + 62 + public function continueParse($bytes) { 63 + $this->buffer .= $bytes; 64 + 65 + $continue = true; 66 + while ($continue) { 67 + switch ($this->state) { 68 + case 'endboundary': 69 + // We've just parsed a boundary. Next, we expect either "--" (which 70 + // indicates we've reached the end of the parts) or "\r\n" (which 71 + // indicates we should read the headers for the next part). 72 + 73 + if (strlen($this->buffer) < 2) { 74 + // We don't have enough bytes yet, so wait for more. 75 + $continue = false; 76 + break; 77 + } 78 + 79 + if (!strncmp($this->buffer, '--', 2)) { 80 + // This is "--" after a boundary, so we're done. We'll read the 81 + // rest of the body (the "epilogue") and discard it. 82 + $this->buffer = substr($this->buffer, 2); 83 + $this->state = 'epilogue'; 84 + 85 + $this->part = null; 86 + break; 87 + } 88 + 89 + if (!strncmp($this->buffer, "\r\n", 2)) { 90 + // This is "\r\n" after a boundary, so we're going to going to 91 + // read the headers for a part. 92 + $this->buffer = substr($this->buffer, 2); 93 + $this->state = 'header'; 94 + 95 + // Create the object to hold the part we're about to read. 96 + $part = new AphrontMultipartPart(); 97 + $this->parts[] = $part; 98 + $this->part = $part; 99 + break; 100 + } 101 + 102 + throw new Exception( 103 + pht('Expected "\r\n" or "--" after multipart data boundary.')); 104 + case 'header': 105 + // We've just parsed a boundary, followed by "\r\n". We are going 106 + // to read the headers for this part. They are in the form of HTTP 107 + // headers and terminated by "\r\n". The section is terminated by 108 + // a line with no header on it. 109 + 110 + if (strlen($this->buffer) < 2) { 111 + // We don't have enough data to find a "\r\n", so wait for more. 112 + $continue = false; 113 + break; 114 + } 115 + 116 + if (!strncmp("\r\n", $this->buffer, 2)) { 117 + // This line immediately began "\r\n", so we're done with parsing 118 + // headers. Start parsing the body. 119 + $this->buffer = substr($this->buffer, 2); 120 + $this->state = 'body'; 121 + break; 122 + } 123 + 124 + // This is an actual header, so look for the end of it. 125 + $header_len = strpos($this->buffer, "\r\n"); 126 + if ($header_len === false) { 127 + // We don't have a full header yet, so wait for more data. 128 + $continue = false; 129 + break; 130 + } 131 + 132 + $header_buf = substr($this->buffer, 0, $header_len); 133 + $this->part->appendRawHeader($header_buf); 134 + 135 + $this->buffer = substr($this->buffer, $header_len + 2); 136 + break; 137 + case 'body': 138 + // We've parsed a boundary and headers, and are parsing the data for 139 + // this part. The data is terminated by "\r\n--", then the boundary. 140 + 141 + // We'll look for "\r\n", then switch to the "bodynewline" state if 142 + // we find it. 143 + 144 + $marker = "\r"; 145 + $marker_pos = strpos($this->buffer, $marker); 146 + 147 + if ($marker_pos === false) { 148 + // There's no "\r" anywhere in the buffer, so we can just read it 149 + // as provided. Then, since we read all the data, we're done until 150 + // we get more. 151 + 152 + // Note that if we're in the preamble, we won't have a "part" 153 + // object and will just discard the data. 154 + if ($this->part) { 155 + $this->part->appendData($this->buffer); 156 + } 157 + $this->buffer = ''; 158 + $continue = false; 159 + break; 160 + } 161 + 162 + if ($marker_pos > 0) { 163 + // If there are bytes before the "\r", 164 + if ($this->part) { 165 + $this->part->appendData(substr($this->buffer, 0, $marker_pos)); 166 + } 167 + $this->buffer = substr($this->buffer, $marker_pos); 168 + } 169 + 170 + $expect = "\r\n"; 171 + $expect_len = strlen($expect); 172 + if (strlen($this->buffer) < $expect_len) { 173 + // We don't have enough bytes yet to know if this is "\r\n" 174 + // or not. 175 + $continue = false; 176 + break; 177 + } 178 + 179 + if (strncmp($this->buffer, $expect, $expect_len)) { 180 + // The next two bytes aren't "\r\n", so eat them and go looking 181 + // for more newlines. 182 + if ($this->part) { 183 + $this->part->appendData(substr($this->buffer, 0, $expect_len)); 184 + } 185 + $this->buffer = substr($this->buffer, $expect_len); 186 + break; 187 + } 188 + 189 + // Eat the "\r\n". 190 + $this->buffer = substr($this->buffer, $expect_len); 191 + $this->state = 'bodynewline'; 192 + break; 193 + case 'bodynewline': 194 + // We've parsed a newline in a body, or we just started parsing the 195 + // request. In either case, we're looking for "--", then the boundary. 196 + // If we find it, this section is done. If we don't, we consume the 197 + // bytes and move on. 198 + 199 + $expect = '--'.$this->boundary; 200 + $expect_len = strlen($expect); 201 + 202 + if (strlen($this->buffer) < $expect_len) { 203 + // We don't have enough bytes yet, so wait for more. 204 + $continue = false; 205 + break; 206 + } 207 + 208 + if (strncmp($this->buffer, $expect, $expect_len)) { 209 + // This wasn't the boundary, so return to the "body" state and 210 + // consume it. (But first, we need to append the "\r\n" which we 211 + // ate earlier.) 212 + if ($this->part) { 213 + $this->part->appendData("\r\n"); 214 + } 215 + $this->state = 'body'; 216 + break; 217 + } 218 + 219 + // This is the boundary, so toss it and move on. 220 + $this->buffer = substr($this->buffer, $expect_len); 221 + $this->state = 'endboundary'; 222 + break; 223 + case 'epilogue': 224 + // We just discard any epilogue. 225 + $this->buffer = ''; 226 + $continue = false; 227 + break; 228 + default: 229 + throw new Exception( 230 + pht( 231 + 'Unknown parser state "%s".\n', 232 + $this->state)); 233 + } 234 + } 235 + } 236 + 237 + public function endParse() { 238 + if ($this->state !== 'epilogue') { 239 + throw new Exception( 240 + pht( 241 + 'Expected "multipart/form-data" parse to end '. 242 + 'in state "epilogue".')); 243 + } 244 + 245 + return $this->parts; 246 + } 247 + 248 + 249 + }
+96
src/aphront/multipartparser/AphrontMultipartPart.php
··· 1 + <?php 2 + 3 + final class AphrontMultipartPart extends Phobject { 4 + 5 + private $headers = array(); 6 + private $value = ''; 7 + 8 + private $name; 9 + private $filename; 10 + private $tempFile; 11 + private $byteSize = 0; 12 + 13 + public function appendRawHeader($bytes) { 14 + $parser = id(new AphrontHTTPHeaderParser()) 15 + ->parseRawHeader($bytes); 16 + 17 + $header_name = $parser->getHeaderName(); 18 + 19 + $this->headers[] = array( 20 + $header_name, 21 + $parser->getHeaderContent(), 22 + ); 23 + 24 + if (strtolower($header_name) === 'content-disposition') { 25 + $pairs = $parser->getHeaderContentAsPairs(); 26 + foreach ($pairs as $pair) { 27 + list($key, $value) = $pair; 28 + switch ($key) { 29 + case 'filename': 30 + $this->filename = $value; 31 + break; 32 + case 'name': 33 + $this->name = $value; 34 + break; 35 + } 36 + } 37 + } 38 + 39 + return $this; 40 + } 41 + 42 + public function appendData($bytes) { 43 + $this->byteSize += strlen($bytes); 44 + 45 + if ($this->isVariable()) { 46 + $this->value .= $bytes; 47 + } else { 48 + if (!$this->tempFile) { 49 + $this->tempFile = new TempFile(getmypid().'.upload'); 50 + } 51 + Filesystem::appendFile($this->tempFile, $bytes); 52 + } 53 + 54 + return $this; 55 + } 56 + 57 + public function isVariable() { 58 + return ($this->filename === null); 59 + } 60 + 61 + public function getName() { 62 + return $this->name; 63 + } 64 + 65 + public function getVariableValue() { 66 + if (!$this->isVariable()) { 67 + throw new Exception(pht('This part is not a variable!')); 68 + } 69 + 70 + return $this->value; 71 + } 72 + 73 + public function getPHPFileDictionary() { 74 + if (!$this->tempFile) { 75 + $this->appendData(''); 76 + } 77 + 78 + $mime_type = 'application/octet-stream'; 79 + foreach ($this->headers as $header) { 80 + list($name, $value) = $header; 81 + if (strtolower($name) == 'content-type') { 82 + $mime_type = $value; 83 + break; 84 + } 85 + } 86 + 87 + return array( 88 + 'name' => $this->filename, 89 + 'type' => $mime_type, 90 + 'tmp_name' => (string)$this->tempFile, 91 + 'error' => 0, 92 + 'size' => $this->byteSize, 93 + ); 94 + } 95 + 96 + }
+45
src/aphront/multipartparser/__tests__/AphrontMultipartParserTestCase.php
··· 1 + <?php 2 + 3 + final class AphrontMultipartParserTestCase extends PhutilTestCase { 4 + 5 + public function testParser() { 6 + $map = array( 7 + array( 8 + 'data' => 'simple.txt', 9 + 'variables' => array( 10 + array('a', 'b'), 11 + ), 12 + ), 13 + ); 14 + 15 + $data_dir = dirname(__FILE__).'/data/'; 16 + foreach ($map as $test_case) { 17 + $data = Filesystem::readFile($data_dir.$test_case['data']); 18 + $data = str_replace("\n", "\r\n", $data); 19 + 20 + $parser = id(new AphrontMultipartParser()) 21 + ->setContentType('multipart/form-data; boundary=ABCDEFG'); 22 + $parser->beginParse(); 23 + $parser->continueParse($data); 24 + $parts = $parser->endParse(); 25 + 26 + $variables = array(); 27 + foreach ($parts as $part) { 28 + if (!$part->isVariable()) { 29 + continue; 30 + } 31 + 32 + $variables[] = array( 33 + $part->getName(), 34 + $part->getVariableValue(), 35 + ); 36 + } 37 + 38 + $expect_variables = idx($test_case, 'variables', array()); 39 + $this->assertEqual($expect_variables, $variables); 40 + } 41 + } 42 + 43 + 44 + 45 + }
+5
src/aphront/multipartparser/__tests__/data/simple.txt
··· 1 + --ABCDEFG 2 + Content-Disposition: form-data; name="a" 3 + 4 + b 5 + --ABCDEFG--
+92
src/aphront/requeststream/AphrontRequestStream.php
··· 1 + <?php 2 + 3 + final class AphrontRequestStream extends Phobject { 4 + 5 + private $encoding; 6 + private $stream; 7 + private $closed; 8 + private $iterator; 9 + 10 + public function setEncoding($encoding) { 11 + $this->encoding = $encoding; 12 + return $this; 13 + } 14 + 15 + public function getEncoding() { 16 + return $this->encoding; 17 + } 18 + 19 + public function getIterator() { 20 + if (!$this->iterator) { 21 + $this->iterator = new PhutilStreamIterator($this->getStream()); 22 + } 23 + return $this->iterator; 24 + } 25 + 26 + public function readData() { 27 + if (!$this->iterator) { 28 + $iterator = $this->getIterator(); 29 + $iterator->rewind(); 30 + } else { 31 + $iterator = $this->getIterator(); 32 + } 33 + 34 + if (!$iterator->valid()) { 35 + return null; 36 + } 37 + 38 + $data = $iterator->current(); 39 + $iterator->next(); 40 + 41 + return $data; 42 + } 43 + 44 + private function getStream() { 45 + if (!$this->stream) { 46 + $this->stream = $this->newStream(); 47 + } 48 + 49 + return $this->stream; 50 + } 51 + 52 + private function newStream() { 53 + $stream = fopen('php://input', 'rb'); 54 + if (!$stream) { 55 + throw new Exception( 56 + pht( 57 + 'Failed to open stream "%s" for reading.', 58 + 'php://input')); 59 + } 60 + 61 + $encoding = $this->getEncoding(); 62 + if ($encoding === 'gzip') { 63 + // This parameter is magic. Values 0-15 express a time/memory tradeoff, 64 + // but the largest value (15) corresponds to only 32KB of memory and 65 + // data encoded with a smaller window size than the one we pass can not 66 + // be decompressed. Always pass the maximum window size. 67 + 68 + // Additionally, you can add 16 (to enable gzip) or 32 (to enable both 69 + // gzip and zlib). Add 32 to support both. 70 + $zlib_window = 15 + 32; 71 + 72 + $ok = stream_filter_append( 73 + $stream, 74 + 'zlib.inflate', 75 + STREAM_FILTER_READ, 76 + array( 77 + 'window' => $zlib_window, 78 + )); 79 + if (!$ok) { 80 + throw new Exception( 81 + pht( 82 + 'Failed to append filter "%s" to input stream while processing '. 83 + 'a request with "%s" encoding.', 84 + 'zlib.inflate', 85 + $encoding)); 86 + } 87 + } 88 + 89 + return $stream; 90 + } 91 + 92 + }
+9
src/aphront/writeguard/AphrontScopedUnguardedWriteCapability.php
··· 1 + <?php 2 + 3 + final class AphrontScopedUnguardedWriteCapability extends Phobject { 4 + 5 + public function __destruct() { 6 + AphrontWriteGuard::endUnguardedWrites(); 7 + } 8 + 9 + }
+267
src/aphront/writeguard/AphrontWriteGuard.php
··· 1 + <?php 2 + 3 + /** 4 + * Guard writes against CSRF. The Aphront structure takes care of most of this 5 + * for you, you just need to call: 6 + * 7 + * AphrontWriteGuard::willWrite(); 8 + * 9 + * ...before executing a write against any new kind of storage engine. MySQL 10 + * databases and the default file storage engines are already covered, but if 11 + * you introduce new types of datastores make sure their writes are guarded. If 12 + * you don't guard writes and make a mistake doing CSRF checks in a controller, 13 + * a CSRF vulnerability can escape undetected. 14 + * 15 + * If you need to execute writes on a page which doesn't have CSRF tokens (for 16 + * example, because you need to do logging), you can temporarily disable the 17 + * write guard by calling: 18 + * 19 + * AphrontWriteGuard::beginUnguardedWrites(); 20 + * do_logging_write(); 21 + * AphrontWriteGuard::endUnguardedWrites(); 22 + * 23 + * This is dangerous, because it disables the backup layer of CSRF protection 24 + * this class provides. You should need this only very, very rarely. 25 + * 26 + * @task protect Protecting Writes 27 + * @task disable Disabling Protection 28 + * @task manage Managing Write Guards 29 + * @task internal Internals 30 + */ 31 + final class AphrontWriteGuard extends Phobject { 32 + 33 + private static $instance; 34 + private static $allowUnguardedWrites = false; 35 + 36 + private $callback; 37 + private $allowDepth = 0; 38 + 39 + 40 + /* -( Managing Write Guards )---------------------------------------------- */ 41 + 42 + 43 + /** 44 + * Construct a new write guard for a request. Only one write guard may be 45 + * active at a time. You must explicitly call @{method:dispose} when you are 46 + * done with a write guard: 47 + * 48 + * $guard = new AphrontWriteGuard($callback); 49 + * // ... 50 + * $guard->dispose(); 51 + * 52 + * Normally, you do not need to manage guards yourself -- the Aphront stack 53 + * handles it for you. 54 + * 55 + * This class accepts a callback, which will be invoked when a write is 56 + * attempted. The callback should validate the presence of a CSRF token in 57 + * the request, or abort the request (e.g., by throwing an exception) if a 58 + * valid token isn't present. 59 + * 60 + * @param callable CSRF callback. 61 + * @return this 62 + * @task manage 63 + */ 64 + public function __construct($callback) { 65 + if (self::$instance) { 66 + throw new Exception( 67 + pht( 68 + 'An %s already exists. Dispose of the previous guard '. 69 + 'before creating a new one.', 70 + __CLASS__)); 71 + } 72 + if (self::$allowUnguardedWrites) { 73 + throw new Exception( 74 + pht( 75 + 'An %s is being created in a context which permits '. 76 + 'unguarded writes unconditionally. This is not allowed and '. 77 + 'indicates a serious error.', 78 + __CLASS__)); 79 + } 80 + $this->callback = $callback; 81 + self::$instance = $this; 82 + } 83 + 84 + 85 + /** 86 + * Dispose of the active write guard. You must call this method when you are 87 + * done with a write guard. You do not normally need to call this yourself. 88 + * 89 + * @return void 90 + * @task manage 91 + */ 92 + public function dispose() { 93 + if (!self::$instance) { 94 + throw new Exception(pht( 95 + 'Attempting to dispose of write guard, but no write guard is active!')); 96 + } 97 + 98 + if ($this->allowDepth > 0) { 99 + throw new Exception( 100 + pht( 101 + 'Imbalanced %s: more %s calls than %s calls.', 102 + __CLASS__, 103 + 'beginUnguardedWrites()', 104 + 'endUnguardedWrites()')); 105 + } 106 + self::$instance = null; 107 + } 108 + 109 + 110 + /** 111 + * Determine if there is an active write guard. 112 + * 113 + * @return bool 114 + * @task manage 115 + */ 116 + public static function isGuardActive() { 117 + return (bool)self::$instance; 118 + } 119 + 120 + /** 121 + * Return on instance of AphrontWriteGuard if it's active, or null 122 + * 123 + * @return AphrontWriteGuard|null 124 + */ 125 + public static function getInstance() { 126 + return self::$instance; 127 + } 128 + 129 + 130 + /* -( Protecting Writes )-------------------------------------------------- */ 131 + 132 + 133 + /** 134 + * Declare intention to perform a write, validating that writes are allowed. 135 + * You should call this method before executing a write whenever you implement 136 + * a new storage engine where information can be permanently kept. 137 + * 138 + * Writes are permitted if: 139 + * 140 + * - The request has valid CSRF tokens. 141 + * - Unguarded writes have been temporarily enabled by a call to 142 + * @{method:beginUnguardedWrites}. 143 + * - All write guarding has been disabled with 144 + * @{method:allowDangerousUnguardedWrites}. 145 + * 146 + * If none of these conditions are true, this method will throw and prevent 147 + * the write. 148 + * 149 + * @return void 150 + * @task protect 151 + */ 152 + public static function willWrite() { 153 + if (!self::$instance) { 154 + if (!self::$allowUnguardedWrites) { 155 + throw new Exception( 156 + pht( 157 + 'Unguarded write! There must be an active %s to perform writes.', 158 + __CLASS__)); 159 + } else { 160 + // Unguarded writes are being allowed unconditionally. 161 + return; 162 + } 163 + } 164 + 165 + $instance = self::$instance; 166 + if ($instance->allowDepth == 0) { 167 + call_user_func($instance->callback); 168 + } 169 + } 170 + 171 + 172 + /* -( Disabling Write Protection )----------------------------------------- */ 173 + 174 + 175 + /** 176 + * Enter a scope which permits unguarded writes. This works like 177 + * @{method:beginUnguardedWrites} but returns an object which will end 178 + * the unguarded write scope when its __destruct() method is called. This 179 + * is useful to more easily handle exceptions correctly in unguarded write 180 + * blocks: 181 + * 182 + * // Restores the guard even if do_logging() throws. 183 + * function unguarded_scope() { 184 + * $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 185 + * do_logging(); 186 + * } 187 + * 188 + * @return AphrontScopedUnguardedWriteCapability Object which ends unguarded 189 + * writes when it leaves scope. 190 + * @task disable 191 + */ 192 + public static function beginScopedUnguardedWrites() { 193 + self::beginUnguardedWrites(); 194 + return new AphrontScopedUnguardedWriteCapability(); 195 + } 196 + 197 + 198 + /** 199 + * Begin a block which permits unguarded writes. You should use this very 200 + * sparingly, and only for things like logging where CSRF is not a concern. 201 + * 202 + * You must pair every call to @{method:beginUnguardedWrites} with a call to 203 + * @{method:endUnguardedWrites}: 204 + * 205 + * AphrontWriteGuard::beginUnguardedWrites(); 206 + * do_logging(); 207 + * AphrontWriteGuard::endUnguardedWrites(); 208 + * 209 + * @return void 210 + * @task disable 211 + */ 212 + public static function beginUnguardedWrites() { 213 + if (!self::$instance) { 214 + return; 215 + } 216 + self::$instance->allowDepth++; 217 + } 218 + 219 + /** 220 + * Declare that you have finished performing unguarded writes. You must 221 + * call this exactly once for each call to @{method:beginUnguardedWrites}. 222 + * 223 + * @return void 224 + * @task disable 225 + */ 226 + public static function endUnguardedWrites() { 227 + if (!self::$instance) { 228 + return; 229 + } 230 + if (self::$instance->allowDepth <= 0) { 231 + throw new Exception( 232 + pht( 233 + 'Imbalanced %s: more %s calls than %s calls.', 234 + __CLASS__, 235 + 'endUnguardedWrites()', 236 + 'beginUnguardedWrites()')); 237 + } 238 + self::$instance->allowDepth--; 239 + } 240 + 241 + 242 + /** 243 + * Allow execution of unguarded writes. This is ONLY appropriate for use in 244 + * script contexts or other contexts where you are guaranteed to never be 245 + * vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS 246 + * if you do not understand the consequences. 247 + * 248 + * If you need to perform unguarded writes on an otherwise guarded workflow 249 + * which is vulnerable to CSRF, use @{method:beginUnguardedWrites}. 250 + * 251 + * @return void 252 + * @task disable 253 + */ 254 + public static function allowDangerousUnguardedWrites($allow) { 255 + if (self::$instance) { 256 + throw new Exception( 257 + pht( 258 + 'You can not unconditionally disable %s by calling %s while a write '. 259 + 'guard is active. Use %s to temporarily allow unguarded writes.', 260 + __CLASS__, 261 + __FUNCTION__.'()', 262 + 'beginUnguardedWrites()')); 263 + } 264 + self::$allowUnguardedWrites = true; 265 + } 266 + 267 + }