2015-08-17 17:00:26 -07: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 .
*
* @ see https :// www . owasp . org / index . php / OWASP_AJAX_Security_Guidelines #Always_return_JSON_with_an_Object_on_the_outside
*
* @ author Igor Wiedler < igor @ wiedler . ch >
*/
class JsonResponse extends Response
{
protected $data ;
protected $callback ;
2015-08-27 12:03:05 -07:00
2017-02-02 16:28:38 -08:00
// Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML.
2015-08-27 12:03:05 -07:00
// 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
protected $encodingOptions = 15 ;
2015-08-17 17:00:26 -07:00
/**
* Constructor .
*
* @ param mixed $data The response data
* @ param int $status The response status code
* @ param array $headers An array of response headers
*/
public function __construct ( $data = null , $status = 200 , $headers = array ())
{
parent :: __construct ( '' , $status , $headers );
if ( null === $data ) {
$data = new \ArrayObject ();
}
$this -> setData ( $data );
}
/**
2017-02-02 16:28:38 -08:00
* Factory method for chainability .
*
* Example :
*
* return JsonResponse :: create ( $data , 200 )
* -> setSharedMaxAge ( 300 );
*
* @ param mixed $data The json response data
* @ param int $status The response status code
* @ param array $headers An array of response headers
*
* @ return static
2015-08-17 17:00:26 -07:00
*/
public static function create ( $data = null , $status = 200 , $headers = array ())
{
return new static ( $data , $status , $headers );
}
/**
* Sets the JSONP callback .
*
* @ param string | null $callback The JSONP callback or null to use none
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*
* @ throws \InvalidArgumentException When the callback name is not valid
*/
public function setCallback ( $callback = null )
{
if ( null !== $callback ) {
2017-02-02 16:28:38 -08:00
// partially token from http://www.geekality.net/2011/08/03/valid-javascript-identifier/
// partially token 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' ;
$reserved = array (
'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' ,
);
2015-08-17 17:00:26 -07:00
$parts = explode ( '.' , $callback );
foreach ( $parts as $part ) {
2017-02-02 16:28:38 -08:00
if ( ! preg_match ( $pattern , $part ) || in_array ( $part , $reserved , true )) {
2015-08-17 17:00:26 -07:00
throw new \InvalidArgumentException ( 'The callback name is not valid.' );
}
}
}
$this -> callback = $callback ;
return $this -> update ();
}
/**
* Sets the data to be sent as JSON .
*
* @ param mixed $data
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*
* @ throws \InvalidArgumentException
*/
public function setData ( $data = array ())
{
2015-08-27 12:03:05 -07:00
if ( defined ( 'HHVM_VERSION' )) {
// HHVM does not trigger any warnings and let exceptions
// thrown from a JsonSerializable object pass through.
// If only PHP did the same...
$data = json_encode ( $data , $this -> encodingOptions );
} else {
try {
if ( PHP_VERSION_ID < 50400 ) {
// PHP 5.3 triggers annoying warnings for some
// types that can't be serialized as JSON (INF, resources, etc.)
// but doesn't provide the JsonSerializable interface.
2016-04-20 09:56:34 -07:00
set_error_handler ( function () { return false ; });
2015-08-27 12:03:05 -07:00
$data = @ json_encode ( $data , $this -> encodingOptions );
} else {
// PHP 5.4 and up wrap exceptions thrown by JsonSerializable
// objects in a new exception that needs to be removed.
// Fortunately, PHP 5.5 and up do not trigger any warning anymore.
if ( PHP_VERSION_ID < 50500 ) {
// Clear json_last_error()
json_encode ( null );
$errorHandler = set_error_handler ( 'var_dump' );
restore_error_handler ();
set_error_handler ( function () use ( $errorHandler ) {
if ( JSON_ERROR_NONE === json_last_error ()) {
return $errorHandler && false !== call_user_func_array ( $errorHandler , func_get_args ());
}
});
}
$data = json_encode ( $data , $this -> encodingOptions );
}
2015-08-17 17:00:26 -07:00
2015-08-27 12:03:05 -07:00
if ( PHP_VERSION_ID < 50500 ) {
restore_error_handler ();
}
} catch ( \Exception $e ) {
if ( PHP_VERSION_ID < 50500 ) {
restore_error_handler ();
}
if ( PHP_VERSION_ID >= 50400 && 'Exception' === get_class ( $e ) && 0 === strpos ( $e -> getMessage (), 'Failed calling ' )) {
throw $e -> getPrevious () ? : $e ;
}
throw $e ;
2015-08-17 17:00:26 -07:00
}
}
if ( JSON_ERROR_NONE !== json_last_error ()) {
2016-04-20 09:56:34 -07:00
throw new \InvalidArgumentException ( json_last_error_msg ());
2015-08-17 17:00:26 -07:00
}
2015-08-27 12:03:05 -07:00
$this -> data = $data ;
2015-08-17 17:00:26 -07:00
return $this -> update ();
}
/**
* Returns options used while encoding data to JSON .
*
* @ return int
*/
public function getEncodingOptions ()
{
return $this -> encodingOptions ;
}
/**
* Sets options used while encoding data to JSON .
*
* @ param int $encodingOptions
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*/
public function setEncodingOptions ( $encodingOptions )
{
$this -> encodingOptions = ( int ) $encodingOptions ;
return $this -> setData ( json_decode ( $this -> data ));
}
/**
* Updates the content and headers according to the JSON data and callback .
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*/
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 );
}
}