2018-02-01 20:01:12 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\HttpFoundation ;
/**
* Response represents an HTTP response in JSON format .
*
* Note that this class does not force the returned JSON content to be an
* object . It is however recommended that you do return an object as it
* protects yourself against XSSI and JSON - JavaScript Hijacking .
*
2022-03-10 11:54:29 +00:00
* @ see https :// github . com / OWASP / CheatSheetSeries / blob / master / cheatsheets / AJAX_Security_Cheat_Sheet . md #always-return-json-with-an-object-on-the-outside
2018-02-01 20:01:12 +00:00
*
* @ author Igor Wiedler < igor @ wiedler . ch >
*/
class JsonResponse extends Response
{
protected $data ;
protected $callback ;
// Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML.
// 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
2022-03-10 11:54:29 +00:00
public const DEFAULT_ENCODING_OPTIONS = 15 ;
2018-02-01 20:01:12 +00:00
protected $encodingOptions = self :: DEFAULT_ENCODING_OPTIONS ;
/**
* @ param mixed $data The response data
* @ param int $status The response status code
* @ param array $headers An array of response headers
* @ param bool $json If the data is already a JSON string
*/
2019-06-11 11:29:32 +00:00
public function __construct ( $data = null , int $status = 200 , array $headers = [], bool $json = false )
2018-02-01 20:01:12 +00:00
{
parent :: __construct ( '' , $status , $headers );
2022-03-10 11:54:29 +00:00
if ( $json && ! \is_string ( $data ) && ! is_numeric ( $data ) && ! \is_callable ([ $data , '__toString' ])) {
throw new \TypeError ( sprintf ( '"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.' , __METHOD__ , get_debug_type ( $data )));
}
2018-02-01 20:01:12 +00:00
if ( null === $data ) {
$data = new \ArrayObject ();
}
$json ? $this -> setJson ( $data ) : $this -> setData ( $data );
}
/**
* Factory method for chainability .
*
* Example :
*
2022-03-10 11:54:29 +00:00
* return JsonResponse :: create ([ 'key' => 'value' ])
2018-02-01 20:01:12 +00:00
* -> setSharedMaxAge ( 300 );
*
2022-03-10 11:54:29 +00:00
* @ param mixed $data The JSON response data
2018-02-01 20:01:12 +00:00
* @ param int $status The response status code
* @ param array $headers An array of response headers
*
* @ return static
2022-03-10 11:54:29 +00:00
*
* @ deprecated since Symfony 5.1 , use __construct () instead .
2018-02-01 20:01:12 +00:00
*/
2022-03-10 11:54:29 +00:00
public static function create ( $data = null , int $status = 200 , array $headers = [])
2018-02-01 20:01:12 +00:00
{
2022-03-10 11:54:29 +00:00
trigger_deprecation ( 'symfony/http-foundation' , '5.1' , 'The "%s()" method is deprecated, use "new %s()" instead.' , __METHOD__ , static :: class );
2018-02-01 20:01:12 +00:00
return new static ( $data , $status , $headers );
}
/**
2022-03-10 11:54:29 +00:00
* Factory method for chainability .
*
* Example :
*
* return JsonResponse :: fromJsonString ( '{"key": "value"}' )
* -> setSharedMaxAge ( 300 );
*
* @ param string $data The JSON response string
* @ param int $status The response status code
* @ param array $headers An array of response headers
*
* @ return static
2018-02-01 20:01:12 +00:00
*/
2022-03-10 11:54:29 +00:00
public static function fromJsonString ( string $data , int $status = 200 , array $headers = [])
2018-02-01 20:01:12 +00:00
{
return new static ( $data , $status , $headers , true );
}
/**
* Sets the JSONP callback .
*
* @ param string | null $callback The JSONP callback or null to use none
*
* @ return $this
*
* @ throws \InvalidArgumentException When the callback name is not valid
*/
2022-03-10 11:54:29 +00:00
public function setCallback ( string $callback = null )
2018-02-01 20:01:12 +00:00
{
if ( null !== $callback ) {
2022-03-10 11:54:29 +00:00
// partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/
2018-02-01 20:01:12 +00:00
// partially taken from https://github.com/willdurand/JsonpCallbackValidator
// JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details.
// (c) William Durand <william.durand1@gmail.com>
$pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u' ;
2019-06-11 11:29:32 +00:00
$reserved = [
2018-02-01 20:01:12 +00:00
'break' , 'do' , 'instanceof' , 'typeof' , 'case' , 'else' , 'new' , 'var' , 'catch' , 'finally' , 'return' , 'void' , 'continue' , 'for' , 'switch' , 'while' ,
'debugger' , 'function' , 'this' , 'with' , 'default' , 'if' , 'throw' , 'delete' , 'in' , 'try' , 'class' , 'enum' , 'extends' , 'super' , 'const' , 'export' ,
'import' , 'implements' , 'let' , 'private' , 'public' , 'yield' , 'interface' , 'package' , 'protected' , 'static' , 'null' , 'true' , 'false' ,
2019-06-11 11:29:32 +00:00
];
2018-02-01 20:01:12 +00:00
$parts = explode ( '.' , $callback );
foreach ( $parts as $part ) {
2018-10-14 19:50:32 +00:00
if ( ! preg_match ( $pattern , $part ) || \in_array ( $part , $reserved , true )) {
2018-02-01 20:01:12 +00:00
throw new \InvalidArgumentException ( 'The callback name is not valid.' );
}
}
}
$this -> callback = $callback ;
return $this -> update ();
}
/**
* Sets a raw string containing a JSON document to be sent .
*
* @ return $this
*/
2022-03-10 11:54:29 +00:00
public function setJson ( string $json )
2018-02-01 20:01:12 +00:00
{
$this -> data = $json ;
return $this -> update ();
}
/**
* Sets the data to be sent as JSON .
*
* @ param mixed $data
*
* @ return $this
*
* @ throws \InvalidArgumentException
*/
2019-06-11 11:29:32 +00:00
public function setData ( $data = [])
2018-02-01 20:01:12 +00:00
{
2018-10-14 19:50:32 +00:00
try {
2018-02-01 20:01:12 +00:00
$data = json_encode ( $data , $this -> encodingOptions );
2018-10-14 19:50:32 +00:00
} catch ( \Exception $e ) {
2022-03-10 11:54:29 +00:00
if ( 'Exception' === \get_class ( $e ) && str_starts_with ( $e -> getMessage (), 'Failed calling ' )) {
2018-10-14 19:50:32 +00:00
throw $e -> getPrevious () ? : $e ;
2018-02-01 20:01:12 +00:00
}
2018-10-14 19:50:32 +00:00
throw $e ;
2018-02-01 20:01:12 +00:00
}
2022-03-10 11:54:29 +00:00
if ( \PHP_VERSION_ID >= 70300 && ( \JSON_THROW_ON_ERROR & $this -> encodingOptions )) {
2019-06-11 11:29:32 +00:00
return $this -> setJson ( $data );
}
2022-03-10 11:54:29 +00:00
if ( \JSON_ERROR_NONE !== json_last_error ()) {
2018-02-01 20:01:12 +00:00
throw new \InvalidArgumentException ( json_last_error_msg ());
}
return $this -> setJson ( $data );
}
/**
* Returns options used while encoding data to JSON .
*
* @ return int
*/
public function getEncodingOptions ()
{
return $this -> encodingOptions ;
}
/**
* Sets options used while encoding data to JSON .
*
* @ return $this
*/
2022-03-10 11:54:29 +00:00
public function setEncodingOptions ( int $encodingOptions )
2018-02-01 20:01:12 +00:00
{
2022-03-10 11:54:29 +00:00
$this -> encodingOptions = $encodingOptions ;
2018-02-01 20:01:12 +00:00
return $this -> setData ( json_decode ( $this -> data ));
}
/**
* Updates the content and headers according to the JSON data and callback .
*
* @ return $this
*/
protected function update ()
{
if ( null !== $this -> callback ) {
// Not using application/javascript for compatibility reasons with older browsers.
$this -> headers -> set ( 'Content-Type' , 'text/javascript' );
return $this -> setContent ( sprintf ( '/**/%s(%s);' , $this -> callback , $this -> data ));
}
// Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback)
// in order to not overwrite a custom definition.
if ( ! $this -> headers -> has ( 'Content-Type' ) || 'text/javascript' === $this -> headers -> get ( 'Content-Type' )) {
$this -> headers -> set ( 'Content-Type' , 'application/json' );
}
return $this -> setContent ( $this -> data );
}
}