Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5958397
MultipartAction.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
8 KB
Referenced Files
None
Subscribers
None
MultipartAction.php
View Options
<?php
namespace
Shellbox\Action
;
use
GuzzleHttp\Psr7\MultipartStream
;
use
GuzzleHttp\Psr7\Stream
;
use
GuzzleHttp\Psr7\Utils
;
use
Psr\Log\LoggerInterface
;
use
Psr\Log\NullLogger
;
use
Shellbox\FileUtils
;
use
Shellbox\Multipart\MultipartReader
;
use
Shellbox\Multipart\MultipartUtils
;
use
Shellbox\Server
;
use
Shellbox\Shellbox
;
use
Shellbox\ShellboxError
;
use
Shellbox\TempDirManager
;
/**
* Base class for actions that share a specific input/output protocol.
*
* @todo Protocol documentation
*/
abstract
class
MultipartAction
{
/** @var TempDirManager */
protected
$tempDirManager
;
/** @var array */
private
$structuredData
;
/** @var string[] */
private
$binaryData
;
/** @var Server */
private
$server
;
/** @var string[] */
private
$headers
;
/** @var LoggerInterface */
protected
$logger
;
/** @var string[] */
private
$inputFiles
=
[];
private
const
COPY_BUFFER_SIZE
=
65536
;
/**
* @param Server $server
*/
public
function
__construct
(
Server
$server
)
{
$this
->
server
=
$server
;
$this
->
logger
=
new
NullLogger
;
}
/**
* @param LoggerInterface $logger
*/
public
function
setLogger
(
LoggerInterface
$logger
)
{
$this
->
logger
=
$logger
;
}
/**
* The entry point for execution of the action
*
* @param string[] $pathParts
*/
public
function
baseExecute
(
$pathParts
)
{
try
{
$this
->
tempDirManager
=
Shellbox
::
createTempDirManager
(
$this
->
getConfig
(
'tempDir'
)
);
$this
->
tempDirManager
->
setLogger
(
$this
->
logger
);
$this
->
processInput
();
$this
->
execute
(
$pathParts
);
}
finally
{
if
(
$this
->
tempDirManager
)
{
$this
->
tempDirManager
->
teardown
();
}
}
}
/**
* Override this to implement the action.
*
* @param string[] $pathParts
*/
abstract
protected
function
execute
(
$pathParts
);
/**
* Override this to provide the action name as used in the URL.
*
* @return string
*/
abstract
protected
function
getActionName
();
/**
* Get a parameter from the request, or throw if it isn't present
*
* @param string $name
* @return mixed
* @throws ShellboxError
*/
protected
function
getRequiredParam
(
$name
)
{
$nonexistent
=
new
\stdClass
;
$result
=
$this
->
getParam
(
$name
,
$nonexistent
);
if
(
$result
===
$nonexistent
)
{
$this
->
error
(
"The $name parameter is required"
);
}
return
$result
;
}
/**
* Get a parameter from the request. Return the specified default if it
* isn't present.
*
* @param string $name
* @param mixed $default
* @return mixed
*/
protected
function
getParam
(
$name
,
$default
=
null
)
{
return
$this
->
structuredData
[
$name
]
??
$this
->
binaryData
[
$name
]
??
$default
;
}
/**
* Get a configuration option, or throw if it doesn't exist.
*
* @param string $name
* @return mixed
* @throws ShellboxError
*/
protected
function
getConfig
(
$name
)
{
return
$this
->
server
->
getConfig
(
$name
);
}
/**
* Erase a configuration option
*
* @param string $name
*/
protected
function
forgetConfig
(
$name
)
{
$this
->
server
->
forgetConfig
(
$name
);
}
/**
* Get a request header of a given name, or null if there was no such header.
*
* @param string $name
* @return string|null
*/
protected
function
getHeader
(
$name
)
{
if
(
$this
->
headers
===
null
)
{
$this
->
headers
=
array_change_key_case
(
getallheaders
(),
CASE_LOWER
);
}
return
$this
->
headers
[
strtolower
(
$name
)]
??
null
;
}
/**
* Throw an error exception
*
* @param string $message
* @param int $code
* @throws ShellboxError
* @return never
*/
protected
function
error
(
$message
,
$code
=
500
)
{
throw
new
ShellboxError
(
$message
,
$code
);
}
/**
* Process the request. Read the POST data, do some generic validation,
* create input files and populate the parameter arrays.
*/
private
function
processInput
()
{
if
(
$_SERVER
[
'REQUEST_METHOD'
]
!==
'POST'
)
{
$this
->
error
(
'The POST method must be used'
,
405
);
}
$auth
=
$this
->
getHeader
(
'Authorization'
);
if
(
$auth
===
null
)
{
$this
->
error
(
'An Authorization header is required'
);
}
// Phan doesn't understand functions that always throw, so thinks that
// $auth could still be null. Maybe fixable -- see comment in
// BlockExitStatusChecker::computeStatusOfCall().
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
if
(
!
preg_match
(
'/^sha256 ([0-9a-z]+)$/'
,
$auth
,
$m
)
)
{
$this
->
error
(
'Invalid Authorization header'
);
}
$authHash
=
$m
[
1
];
$ctype
=
$this
->
getHeader
(
'Content-Type'
);
if
(
!
preg_match
(
'/^multipart
\/
mixed/i'
,
$ctype
,
$m
)
)
{
$this
->
error
(
'The Content-Type must be multipart/mixed'
,
415
);
}
$boundary
=
MultipartUtils
::
extractBoundary
(
$ctype
);
if
(
$boundary
===
false
||
$boundary
===
''
)
{
$this
->
error
(
'boundary is a required parameter of Content-Type'
);
}
$inputFile
=
FileUtils
::
openInputFile
(
'php://input'
);
$multipartReader
=
new
MultipartReader
(
new
Stream
(
$inputFile
),
$boundary
,
$this
->
getConfig
(
'secretKey'
)
);
$preamble
=
$multipartReader
->
readPreamble
();
if
(
trim
(
$preamble
)
!==
''
)
{
$this
->
error
(
'The multipart preamble must be empty, otherwise a '
.
'fraudulent boundary with a replayed body could be used to send '
.
'unauthorized requests'
);
}
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while
(
(
$headers
=
$multipartReader
->
readPartHeaders
()
)
!==
false
)
{
if
(
!
isset
(
$headers
[
'content-disposition'
]
)
)
{
$this
->
error
(
'Part has no Content-Disposition'
,
400
);
}
$disposition
=
$headers
[
'content-disposition'
];
if
(
$disposition
[
'type'
]
===
'json-data'
)
{
$this
->
structuredData
=
$multipartReader
->
readPartAsJson
(
$headers
);
}
elseif
(
$disposition
[
'type'
]
===
'form-data'
)
{
if
(
!
isset
(
$disposition
[
'name'
]
)
)
{
$this
->
error
(
"multipart form-data requires name"
);
}
$name
=
$disposition
[
'name'
];
$this
->
binaryData
[
$name
]
=
$multipartReader
->
readPartAsString
();
}
elseif
(
$disposition
[
'type'
]
===
'attachment'
)
{
$this
->
processFile
(
$multipartReader
,
$headers
);
}
else
{
$this
->
error
(
"Unknown content disposition type"
);
}
}
$multipartReader
->
readEpilogue
();
if
(
$this
->
getRequiredParam
(
'action'
)
!==
$this
->
getActionName
()
)
{
$this
->
error
(
"The URL action must match the HMAC-verified parameter"
);
}
if
(
!
hash_equals
(
$multipartReader
->
getHash
(),
$authHash
)
)
{
$this
->
error
(
"HMAC signature verification failed"
);
}
}
/**
* Extract a single file from the MultipartReader
*
* @param MultipartReader $multipartReader
* @param array $headers The part headers
*/
private
function
processFile
(
$multipartReader
,
$headers
)
{
if
(
!
isset
(
$headers
[
'content-disposition'
][
'filename'
]
)
)
{
$this
->
error
(
'Part has no filename'
);
}
$fileName
=
Shellbox
::
normalizePath
(
$headers
[
'content-disposition'
][
'filename'
]
);
$path
=
$this
->
tempDirManager
->
preparePath
(
$fileName
);
$file
=
FileUtils
::
openOutputFile
(
$path
);
$multipartReader
->
copyPartToStream
(
Utils
::
streamFor
(
$file
)
);
$this
->
inputFiles
[]
=
$fileName
;
}
/**
* Write a standard result
*
* @param array $structuredData JSON serializable data
* @param string[] $binaryData An array of strings to be sent as multipart
* parts
* @param string[] $files The names of the output files relative to the
* working directory
*/
protected
function
writeResult
(
$structuredData
,
$binaryData
=
[],
$files
=
[]
)
{
$boundary
=
Shellbox
::
getUniqueString
();
header
(
"Content-Type: multipart/mixed; boundary=
\"
$boundary
\"
"
);
$parts
=
[];
$structuredData
[
'log'
]
=
$this
->
server
->
flushLogBuffer
();
$parts
[]
=
[
'name'
=>
'json-data'
,
'headers'
=>
[
'Content-Type'
=>
'application/json'
,
'Content-Disposition'
=>
'json-data'
,
],
'contents'
=>
Shellbox
::
jsonEncode
(
$structuredData
)
];
foreach
(
$binaryData
as
$name
=>
$value
)
{
$parts
[]
=
[
'name'
=>
$name
,
'contents'
=>
$value
];
}
foreach
(
$files
as
$name
)
{
// phpcs:ignore Generic.PHP.NoSilencedErrors
$stream
=
@
fopen
(
$this
->
tempDirManager
->
getPath
(
$name
),
'r'
);
if
(
$stream
)
{
$parts
[]
=
[
'name'
=>
$name
,
'headers'
=>
[
'Content-Type'
=>
'application/octet-stream'
,
'Content-Disposition'
=>
"attachment; name=
\"
$name
\"
"
],
'contents'
=>
Utils
::
streamFor
(
$stream
)
];
}
}
$multipartStream
=
new
MultipartStream
(
$parts
,
$boundary
);
while
(
!
$multipartStream
->
eof
()
)
{
echo
$multipartStream
->
read
(
self
::
COPY_BUFFER_SIZE
);
}
}
/**
* Get the names of the received files, relative to the temporary directory
*
* @return string[]
*/
protected
function
getReceivedFileNames
()
{
return
$this
->
inputFiles
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 05:45 (2 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
46/d5/53e16a06beb93e27262eddc643c1
Default Alt Text
MultipartAction.php (8 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment