REST support


(Digital God) #1

Hi, maybe there is some requests like this, but i haven't found them.

So, it is possible to add some restful support to yii? for example to CHttpRequest class?add some function to filter/secure request data before using them.

i've found only one way to get PUT and DELETE request in php

file_get_contents("php://input");

so, there is simple code



    $request->method = $_SERVER['REQUEST_METHOD'];


    $request->get = $_GET;


    $request->post = $_POST;


    if(  $request->method == "POST" ||


      $request->method == "PUT" )


    {


      $request->input = file_get_contents('php://input');


 


      if($request->method == "POST") {


        $request->post = $_POST;


      } else {


        $request->put = self::getPutParameters($request->input);


      }


     }


$request is CHttpRequest object

and getPutParameters is something like



  getPutParameters($input) {


    $data = $input;


    if(function_exists('mb_parse_str')) {


     mb_parse_str($data, $outputdata);


    } else {


      parse_str($data, $outputdata);


    }


      return $outputdata;


  }


i think many people will need this feature


(Qiang Xue) #2

Yes, we already have a ticket requesting for this feature. We will add support for it.


(Jerryablan) #3

Here's a helper object I threw together to pull http data. Hope it helps.

Usage:



$_sResults = CAppHelpers::getRequest( 'http://www.yiiframework.com' );




<?php


/**


 * CAppHelpers class file.


 *


 * @author Jerry Ablan <jablan@pogostick.com>


 * @link http://www.pogostick.com.com/


 * @copyright Copyright &copy; 2009 Pogostick, LLC


 * @license http://www.pogostick.com/license/


 */





/**


 * CHelpers provides helper methods


 *


 * @author Jerry Ablan <jablan@pogostick.com>


 * @version $Id: CAppHelpers.php 17 2009-03-21 21:46:58Z jablan $


 * @package system.web.helpers


 * @since 1.0.3


 */


class CAppHelpers


{


	/**


	 * Make the REST request


	 *


	 * @param unknown_type $sUrl


	 * @param unknown_type $sQueryString


	 * @return unknown


	 */


	public static function getRequest( $sUrl, $sQueryString = '', $sNewAgent = "" )


	{


		$sAgent = $sNewAgent;





		if ( $sNewAgent == "" )


			$sAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506; InfoPath.3)";





		if ( function_exists( 'curl_init' ) )


		{


			// Use CURL if installed...


			$oConn = curl_init();


			curl_setopt( $oConn, CURLOPT_URL, $sUrl . ( $sQueryString != '' ? "?" . $sQueryString : '' ) );


			curl_setopt( $oConn, CURLOPT_RETURNTRANSFER, true );


			curl_setopt( $oConn, CURLOPT_USERAGENT, $sAgent );


			curl_setopt( $oConn, CURLOPT_TIMEOUT, 60 );


			curl_setopt( $oConn, CURLOPT_FOLLOWLOCATION, true );


			$sResult = curl_exec( $oConn );


			curl_close( $oConn );


		}


		else


		{


			// Non-CURL based version...


			$oContext =


				array('http' =>


					array('method' => 'POST',


						'header' => 'Content-type: application/x-www-form-urlencoded'."rn".


						'User-Agent: ' . $sAgent ."rn".


						'Content-length: ' . strlen($post_string),


						'content' => $post_string)


					);





			$oContextId = stream_context_create( $oContext );





			$oSocket = fopen( $sUrl . "?" . $sQueryString, 'r', false, $oContextId );





			if ( $oSocket )


			{


				$sResult = '';





				while ( !feof( $oSocket ) )


					$sResult .= fgets( $oSocket, 4096 );





				fclose( $oSocket );


			}


		}





		return( $sResult );


	}





	/**


	* Parse HTML field for a tag...


	* 


	* @param mixed $sData


	* @param mixed $sTag


	* @param mixed $sTagEnd


	* @param mixed $iStart


	* @param mixed $sNear


	* @return string


	*/


	public static function suckTag( $sData, $sTag, $sTagEnd, $iStart = 0, $sNear = null )


	{


		$_sResult = "";


		$_l = strlen( $sTag );





		//	If near value given, get position of that as start


		if ( $sNear != null )


		{


			$_iStart = stripos( $sData, $sNear, $iStart );


			if ( $_iStart >= 0 )


				$iStart = $_iStart + strlen( $sNear );


		}





		$_i = stripos( $sData, $sTag, $iStart );


		$_k = strlen( $sTagEnd );





		if ( $_i >= 0 )


		{


			$_j = stripos( $sData, $sTagEnd, $_i + $_l );





			if ( $_j >= 0 )


			{


				$iStart = $_i;


				$_sResult = substr( $sData, $_i + $_l,  $_j - $_i - $_l );


			}





			return( trim( $_sResult ) );


		}





		return( null );


	}


	


	/**


	* Checks to see if the passed in data is an Url


	* 


	* @param string $sData


	* @return bool


	*/


	public static function isUrl( $sData )


	{


		return( ( @parse_url( $sData ) ) ? TRUE : FALSE );


	}


}



(Weizhuo) #4

Be aware that prior to php 5.2.10. and curl pre-7.19.4 versions CURLOPT_FOLLOWLOCATION =true will unconditionally follow to all protocols supported. This includes FILE:///etc/path/to/passwords

Yes, PHP 5.2.10 not PHP 5.2.1


(Digital God) #5

the best solution is input stream - it is faster then curl, and users don't need to install curl extension.


(Info) #6

Cant add a more secure script? something that detect sql injection, or mysql stuff, or whatever javascript?


(Andrekramer84) #7

I added some adjustment for those interested:

  • Authentication put in Init since all of the actions would require some authentication

  • The instance of the models are auto created, no more long switches, through

    $m = $this->_model; //string name of the model

    $model = $m::model()->findByPk($this->_id);

Just fill in your model names in the array.

  • Error if the model doesnt exist in the init

  • body message of error is a seperate view

  • Authentication through UserIdentity Component

  • Use of Yii::app()->request->getParam() instead of $_GET

  • removed some unnessesary is_null checks

  • instead of loading the model and then perfoming a delete, I choose to use deleteByPK method.

I find the controller more cleaner now. Still there is room for improvement, maybe people can help me out:

  • Instead of calling _checkAuth() from the init. It would be cleaner if authentication goes through accessrules. I couldnt get it to work though.

  • Models that can be used in the REST API are now defined in allowed_models array. Neater would be using accessrules too, or read which models Yii has access too…

  • Status code in seperate component or behaviour.

Small note. My models are all capitalized, thats why I had to add a ucfirst to the model param. The model param is lowercase by default (and I didnt want to make the url casesensitive).

Note: This is not my code, kudos goes to the original poster(Digital God). I just adjusted to suit my needs and where I saw room for improvement

The code:

RestController.php (probally the only thing you need to adjust is the allowed_models array)


<?php

/**

 * Controller is the customized base controller class.

 * All controller classes for this application should extend from this base class.

 */

class RestController extends CController

{

    private $_model = false;

    private $_id = false;

    private $allowed_models = array('Locations');


    public $format = "json";

    public $layout = false;

    

    const APPLICATION_ID = 'ASCCPE';

    /*

    public function filters()

	{

		return array(

			'accessControl', // perform access control for CRUD operations

		);

	}

	public function accessRules()

	{

		return array(

            array('allow', // allow admin user to perform 'admin' and 'delete' actions

                'verbs'=>array('GET'),

				'users'=>array('*'),

             ),

			array('allow', // allow admin user to perform 'admin' and 'delete' actions

				'actions'=>array('list','delete','create','update'),

                'expression'=> $this->_checkAuth(),

                 'message'=>'Access Denied.'

			),

			array('deny',  // deny all users

				'users'=>array('*'),


			),

		);

	} */




    public function init(){

        //load the model from GET/POST

        $this->_checkAuth();

        $model_request = Yii::app()->request->getParam('model', false);


        if(!empty($model_request)){


            //fix. Models in my db begin with a capital letter, but requests are auto lowercased.

            //either remove this line, update the database to be lowercased or make all the models in allowed_models lowercased.

            //I prefered keeping original model name and just uppercase the request. Your pick.

            $model_request = ucfirst($model_request);


            //check if the model is allowed to be reached (in the array of allowed_models)

            //could be pretified by looking at accessrules somehow...

            if(in_array($model_request, $this->allowed_models)){

                $this->_model = $model_request;

                //load possible id from GET/POST, else set to false

                $this->_id = Yii::app()->request->getParam('id', false);

            } else {

                //throw a 501 if the model doesn't exist/isn't allowed

                $this->_model = false;

                $this->_sendResponse(501, 'Error: Mode <b>'.$_SERVER['REQUEST_METHOD'].'</b> is not implemented for model <b>'.$this->_model.'</b>');

                exit();

            }

        }

    }

    public function actionIndex()

    {

       print "test";

    }

     public function actionView()

    {

       //no need to check if id is send. If no id is defined, route will send it to list.

       $m = $this->_model;

       $model = $m::model()->findByPk($this->_id);

       if(!$model) {

            $this->_sendResponse(404, 'No Item found with id '. $this->_id);

       } else {

            $this->_sendResponse(200, $this->_getObjectEncoded($this->_model, $model->attributes));

       }

        exit;

    }

    public function actionCreate()

    {

       $m = $this->_model;

       $model = new $m;


        // Try to assign POST values to attributes

        foreach($_POST as $var=>$value) {

            // Does the model have this attribute?

            if($model->hasAttribute($var)) {

                $model->$var = $value;

            } else {

                // No, raise an error

                $this->_sendResponse(500, sprintf('Parameter <b>%s</b> is not allowed for model <b>%s</b>', $var, $this->_model) );

            }

        }

        if($model->save()) {

            // Saving was OK

            $this->_sendResponse(200, $this->_getObjectEncoded($this->_model, $model->attributes) );

        } else {

            // Errors occurred

            $msg = "<h1>Error</h1>";

            $msg .= sprintf("Couldn't create model <b>%s</b>", $this->_model);

            $msg .= "<ul>";

            foreach($model->errors as $attribute=>$attr_errors) {

                $msg .= "<li>Attribute: $attribute</li>";

                $msg .= "<ul>";

                foreach($attr_errors as $attr_error) {

                    $msg .= "<li>$attr_error</li>";

                }

                $msg .= "</ul>";

            }

            $msg .= "</ul>";

            $this->_sendResponse(500, $msg );

        }


        var_dump($_REQUEST);

    }

    public function actionUpdate()

    {

        // Check if id was submitted via GET

        if(!$this->_id){

            $this->_sendResponse(500, 'Error: Parameter <b>id</b> is missing' );

        }

        // Get PUT parameters

        parse_str(file_get_contents('php://input'), $put_vars);


       $m = $this->_model;

       $model = $m::model()->findByPk($this->_id);

       if(!$model) {

           $this->_sendResponse(404, 'No Item found with id '. $this->_id);

       } else {

            $this->_sendResponse(200, $this->_getObjectEncoded($this->_model, $model->attributes));

       }

        // Try to assign PUT parameters to attributes

        foreach($put_vars as $var=>$value) {

            // Does model have this attribute?

            if($model->hasAttribute($var)) {

                $model->$var = $value;

            } else {

                // No, raise error

                $this->_sendResponse(500, sprintf('Parameter <b>%s</b> is not allowed for model <b>%s</b>', $var, $this->_model) );

            }

        }

        // Try to save the model

        if($model->save()) {

            $this->_sendResponse(200, sprintf('The model <b>%s</b> with id <b>%s</b> has been updated.', $this->_model,  $this->_id) );

        } else {

            $msg = "<h1>Error</h1>";

            $msg .= sprintf("Couldn't update model <b>%s</b>", $this->_model);

            $msg .= "<ul>";

            foreach($model->errors as $attribute=>$attr_errors) {

                $msg .= "<li>Attribute: $attribute</li>";

                $msg .= "<ul>";

                foreach($attr_errors as $attr_error) {

                    $msg .= "<li>$attr_error</li>";

                }

                $msg .= "</ul>";

            }

            $msg .= "</ul>";

            $this->_sendResponse(500, $msg );

        }

    }

    public function actionDelete()

    {

        // Check if id was submitted via GET

        if(!$this->_id){

            $this->_sendResponse(500, 'Error: Parameter <b>id</b> is missing' );

        }

       $m = $this->_model;

       //delete without loading complete model

       if($m::model()->deleteByPk($this->_id)){

          $this->_sendResponse(200, sprintf("Model <b>%s</b> with ID <b>%s</b> has been deleted.",$this->_model, $this->_id) );

       } else {

          //delete failed

           $this->_sendResponse(500, sprintf("Error: Couldn't delete model <b>%s</b> with ID <b>%s</b>.",$this->_model, $this->_id) );

       }

    }

    public function actionList()

    {

       $m = $this->_model;

       $models = $m::model()->findAll();


        if(is_null($models)) {

            $this->_sendResponse(200, sprintf('No items where found for model <b>%s</b>', $this->_model) );

        } else {

            $rows = array();

            foreach($models as $model)

                $rows[] = $model->attributes;


            $this->_sendResponse(200, CJSON::encode($rows));

        }

    }


    private function _sendResponse($status = 200, $body = '', $content_type = 'text/html')

    {

        $status_header = 'HTTP/1.1 ' . $status . ' ' . $this->_getStatusCodeMessage($status);

        // set the status

        header($status_header);

        // set the content type

        header('Content-type: ' . $content_type);


        // pages with body are easy

        if($body != '')

        {

            // send the body

            echo $body;

            exit;

        }

        // we need to create the body if none is passed

        else

        {

            // create some body messages

            $message = '';


            // this is purely optional, but makes the pages a little nicer to read

            // for your users.  Since you won't likely send a lot of different status codes,

            // this also shouldn't be too ponderous to maintain

            switch($status)

            {

                case 401:

                    $message = 'You must be authorized to view this page.';

                    break;

                case 404:

                    $message = 'The requested URL ' . $_SERVER['REQUEST_URI'] . ' was not found.';

                    break;

                case 500:

                    $message = 'The server encountered an error processing your request.';

                    break;

                case 501:

                    $message = 'The requested method is not implemented.';

                    break;

            }


            // servers don't always have a signature turned on (this is an apache directive "ServerSignature On")

            $signature = ($_SERVER['SERVER_SIGNATURE'] == '') ? $_SERVER['SERVER_SOFTWARE'] . ' Server at ' . $_SERVER['SERVER_NAME'] . ' Port ' . $_SERVER['SERVER_PORT'] : $_SERVER['SERVER_SIGNATURE'];


            $this->render('message', array(

                 'status' => $status,

                 'statusmessage' => $this->_getStatusCodeMessage($status),

                 'signature' => $signature

               ));

            exit;

        }

    }

    private function _getStatusCodeMessage($status)

    {

        // these could be stored in a .ini file and loaded

        // via parse_ini_file()... however, this will suffice

        // for an example

        $codes = Array(

            100 => 'Continue',

            101 => 'Switching Protocols',

            200 => 'OK',

            201 => 'Created',

            202 => 'Accepted',

            203 => 'Non-Authoritative Information',

            204 => 'No Content',

            205 => 'Reset Content',

            206 => 'Partial Content',

            300 => 'Multiple Choices',

            301 => 'Moved Permanently',

            302 => 'Found',

            303 => 'See Other',

            304 => 'Not Modified',

            305 => 'Use Proxy',

            306 => '(Unused)',

            307 => 'Temporary Redirect',

            400 => 'Bad Request',

            401 => 'Unauthorized',

            402 => 'Payment Required',

            403 => 'Forbidden',

            404 => 'Not Found',

            405 => 'Method Not Allowed',

            406 => 'Not Acceptable',

            407 => 'Proxy Authentication Required',

            408 => 'Request Timeout',

            409 => 'Conflict',

            410 => 'Gone',

            411 => 'Length Required',

            412 => 'Precondition Failed',

            413 => 'Request Entity Too Large',

            414 => 'Request-URI Too Long',

            415 => 'Unsupported Media Type',

            416 => 'Requested Range Not Satisfiable',

            417 => 'Expectation Failed',

            500 => 'Internal Server Error',

            501 => 'Not Implemented',

            502 => 'Bad Gateway',

            503 => 'Service Unavailable',

            504 => 'Gateway Timeout',

            505 => 'HTTP Version Not Supported'

        );


        return (isset($codes[$status])) ? $codes[$status] : '';

    }

    public function _checkAuth()

    {

        // Check if we have the USERNAME and PASSWORD HTTP headers set?

        if(!(isset($_SERVER['HTTP_X_'.self::APPLICATION_ID.'_USERNAME']) and isset($_SERVER['HTTP_X_'.self::APPLICATION_ID.'_PASSWORD']))) {

            // Error: Unauthorized

            $this->_sendResponse(401);

            exit;

        }

        $username = $_SERVER['HTTP_X_'.self::APPLICATION_ID.'_USERNAME'];

        $password = $_SERVER['HTTP_X_'.self::APPLICATION_ID.'_PASSWORD'];


        // Find the user

        $user_identity = new UserIdentity($username,$password);

        $user_identity->authenticate();


        if($user_identity->errorCode===UserIdentity::ERROR_USERNAME_INVALID) {

            // Error: Unauthorized

            $this->_sendResponse(401, 'Error: User Name is invalid');

        } else if($user_identity->errorCode===UserIdentity::ERROR_PASSWORD_INVALID) {

            // Error: Unauthorized

           $this->_sendResponse(401, 'Error: User Password is invalid');

            //return false;

        }

    }

    public function actionAccessDenied(){

         $this->_sendResponse(401, 'Error: User Password is invalid');

    }

    private function _getObjectEncoded($model, $array)

    {

        if(isset($_GET['format']))

            $this->format = $_GET['format'];


        if($this->format=='json')

        {

            return CJSON::encode($array);

        }

        elseif($this->format=='xml')

        {

            $result = '<?xml version="1.0">';

            $result .= "\n<$model>\n";

            foreach($array as $key=>$value)

                $result .= "    <$key>".utf8_encode($value)."</$key>\n";

            $result .= '</'.$model.'>';

            return $result;

        }

        else

        {

            return;

        }

    }

}

the UserIdentity Component (uses User model, adjust to your needs)


class UserIdentity extends CUserIdentity

{

    private $_id;

	public function authenticate()

	{

        $record=Users::model()->findByAttributes(array('Username'=>$this->username));

        if($record===null)

            $this->errorCode=self::ERROR_USERNAME_INVALID;

        else if($record->Password!==md5($this->password))

            $this->errorCode=self::ERROR_PASSWORD_INVALID;

        else

        {

            $this->_id=$record->UserId;

            $this->errorCode=self::ERROR_NONE;

        }

        return !$this->errorCode;

	}

    public function getId()

    {

        return $this->_id;

    }

}

the message view (put in view/rest)


<!DOCTYPE HTML>

<html>

    <head>

        <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

        <title><?php echo $status . ' ' . $statusmessage ?></title>

    </head>

    <body>

        <h1><?php echo $statusmessage; ?></h1>

        <p><?php echo $message; ?></p>

        <hr />

        <address><?php echo $signature; ?></address>

    </body>

</html>

the url rewrite I changed a little bit (basicly I switched the default urlmanagement around to allow the restcontroller be put in a module).


		'urlManager'=>array(

			'urlFormat'=>'path',

            'showScriptName'=>false,

			'rules'=>array(

                  // REST patterns for API module

                array('rest/view', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'GET'),

                array('rest/list', 'pattern'=>'api/<model:\w+>', 'verb'=>'GET'),

                array('rest/update', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'PUT'),

                array('rest/delete', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'DELETE'),

                array('rest/create', 'pattern'=>'api/<model:\w+>', 'verb'=>'POST'),

                //default controller/action path

                '<controller:\w+>/<id:\d+>'=>'<controller>/view',

				'<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',

				'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',

                

			),

		),