Flourish PHP Unframework

fRequest

The fRequest class is a static class that allows access to data sent via GET, POST, PUT and DELETE HTTP requests. It also provides functionality to determine the HTTP method used and retrieve relevant Accept and Accept-Language values sent by the browser.

Getting Values

While fRequest does more than just get values, most developers will be primarily interested in the two methods get() and getValid(). Both methods pull values from the $_GET and $_POST superglobals and the php://input stream (for PUT and DELETE requests).

The get() method will pull a value, but you can also typecast the value and provide a default. The first parameter, $key, specifies the key to get the value for. The second (optional) parameter, $cast_to, will set the data type of the value. It accepts any type string that is valid for the PHP function settype(). NULL can be used if no type casting should happen.

The third (optional) parameter, $default_value, specifies what should be returned if the key is not found in $_POST, $_GET or php://input. The fourth (optional) parameter, $use_default_for_blank, causes the $default value to be returned if a blank string value was provided for the key, or the key is not found.

Here are some examples:

<?php
// The first_name field will be returned in whatever format was submitted - string or array
$first_name = fRequest::get('first_name');

// An array will always be returned, defaulting to an array of 1 if no value is provided
$group_ids  = fRequest::get('group_ids', 'array', array(1));

// Integer return value - 1 will be returned if no value, but a blank value will be cast to 0
$user_id    = fRequest::get('user_id', 'integer', 1);

// Integer return value -  1 will be returned if no value or a blank string is provided
$total_cost = fRequest::get('total_cost', 'integer', 1, TRUE);
?>

Input Filtering by Typecasting (Security)

An important aspect of the get() method is to use the $cast_to parameter whenever possible. This helps to restrict data coming in and is part of creating an effective solution against cross-site scripting.

// IDs that can only ever be integers should be cast to integers
$blog_id = fRequest::get('blog_id', 'integer');

Currently the following data types are supported:

Special Data Type Handling

When accepting information from the user, different data types have to be handled differently. Of special note are arrays, binary, booleans, dates/times/timestamps, integers and strings.

Arrays

For arrays, PHP will automatically create an array when input field names end with []. Thus if you wanted to capture an array of groups ids, you could create a checkbox for each group id and give each one the name group_ids[]. When PHP parses the request data it will join all group_ids[] values into an array and assign it to the key groups_ids in the appropriate superglobal.

The array features mentioned above are built into PHP, however Flourish takes the array processing a step further. If a value is to be cast to an array, and is a string that contains commas, the string will be exploded on the comma character. Additionally, if an array contains a single entry of either a blank string or a NULL value, Flourish will return an empty array. This prevents having to manually filter arrays for meaningless values.

See strict arrays to create a single-dimensional array of a specific data type.

Binary

Binary data does not have any modification made to it when it is returned. There is no guarantee what encoding it will be in, and it may even contain null bytes.

Booleans

Booleans are interpreted from string values in as logical a way as possible. Empty values (according to PHPs empty() function) and the strings 'f', 'false' and 'no' (case-insensitive) will result in a FALSE value. This is obviously also the case if the key is not present in the request. Any other string value such as '1', 't', 'true', 'yes', etc. will be interpreted as a TRUE value.

Dates/Times/Timestamps

When the date, time and timestamp data types are specified, the returned values will be fDate, fTime and fTimestamp objects, respectively.

Integers

When the integer or int type is specified, all values are cast to a real PHP integer, except for large integer values, which are kept as strings. Large integer values are value that can not be represented by a PHP integer. If a real PHP integer is always required, the integer! type may be used instead. This type may cause the modification of some large integer values.

Strings

All strings that are passed through the get() method are expected to be UTF-8 and any invalid UTF-8 characters are removed by passing the data through fUTF8::clean(). In addition to invalid UTF-8 byte sequences, all low byte non-printable characters are removed. This is true even if a value is not explicitly cast to a string, or if the string is contained inside of an array. If you need raw data, use the binary type.

Allowing Null

When typecasting values, it is sometimes useful for the absence of a value to return NULL, while still casting all non-NULL values. By adding a ? to the end of the data type, NULL will be returned if the key is not present in the request data, or if the value is an empty string ''.

// Get an integer or NULL
$count = fRequest::get('count', 'integer?');

// Get an fDate object or NULL
$date  = fRequest::get('date', 'date?');

Strict Arrays

The array type ensures that the value returned will be an array containing string values, with an unlimited number of dimensions. It is also possible to add [] to the end of any other data type to return a single-dimensional array containing values of that type.

// Return a single dimensional array of integers
$ids = fRequest::get('ids', 'integer[]');

Restricting Valid Values

The method getValid() simplifies the process a great deal by taking exactly two parameters, $key, the key to request the value for and $valid_values, an array of permissible values. If the value is not one of the valid value, the first valid value will be picked. Here is an example:

<?php
// $action will get the value 'list' if no value was present
$action = fRequest::getValid('action', array('list', 'search'));
?>

Escaping for HTML Output

The static methods encode() and prepare() provide a simple short to calling get() and then wrapping the result in fHTML::encode() or fHTML::prepare() respectively. These two methods have the exact same signature as get() and don't perform any processing other than passing the resulting value to fHTML.

// Output a trusted value from the request
echo fRequest::prepare('name');

// Output an untrusted value from the request
echo fRequest::encode('name');

Just as with fHTML::prepare(), be sure to only ever use prepare() if the user input can be trusted. prepare() allows for HTML to be inserted into the page unescaped. If the input is untrusted, the encode() method should be used instead.

Setting Values

Sometimes when dealing with different input types, it is useful to be able to alter or fix the request data. The method set() allows a new value to be set to any key. It accepts two parameters, the $key to set and the $value to assign.

if ($datetime = fRequest::get('date_time')) {
    fRequest::set('date', date('Y-m-d', strtotime($datetime)));
    fRequest::set('time', date('g:ia',  strtotime($datetime)));
}

Array Dereferencing

When a request value is an array, it is possible to use array dereference syntax in the field name to access a specific array key. This syntax works with get(), getValid(), set(), check(), encode() and prepare().

An input using array notation will automatically be converted to an array when PHP processes the request. This means the following inputs:

<input name="users[first_name]" value="John" />
<input name="users[last_name]" value="Smith" />

will cause the $_POST superglobal to contain:

array(
    'user' => array(
        'first_name' => 'John',
        'last_name'  => 'Smith'
    )
)

fRequest uses array dereference syntax that is identical to the <input> tag syntax.

// This will echo John
echo fRequest::get('user[first_name]');

Array dereferencing can be any number of layers deep.

echo fRequest::get('user[groups][0][name]');

Preventing CSRF (Security)

Cross-site request forgery (CSRF) is a technique where malicious websites will take advantage of the fact that a user has a valid session on a site, and will generate unauthorized requests on their behalf. These unauthorized requests can take the form of either GET or POST requests and can affect any page that does not validate the request properly.

GET request exploits are very easy to implement by taking advantage of the src attribute of various HTML tags. Imagine the HTML below living on http://example2.com:

<img src="http://example.com/my_messages/delete_all.php" />

If the script located at /my_messages/delete_all.php allowed a GET request to cause all messages to delete and the user who visited example2.com was logged into example.com, all of their messages would be deleted.

POST request exploits are slightly more difficult to take advantage of since a user will have to actually submit a form. Imagine the HTML below living on http://example2.com:

<p>
    Sign up for a free MP3 player!
</p>
<form action="http://example.com/my_messages/delete_all.php" method="post">
    <p>
        <label for="email">Your Email</label>
        <input id="email" name="email" value="" />   
    </p>
    <p>
        <input type="submit" value="Sign Up!" />
    </p>
</form>

This form would work even if the script requires a POST request, however it requires convincing the user to submit a form or use javascript to automatically trigger it.

Obviously quite a number of things can help to prevent there CSRF attacks, however the best security is implemented by giving the user a single-use crytographically random token for them to resubmit with the request. This requires that the user request the page first (to get the token) and then resubmit it.

Since the attack relies on the user being logged in to a site in their browser, the attacking site will not be able to use a server-side request to retrieve the page, and thus the token. In addition, browsers prevent javascript from requesting pages across domains, so that precludes an attacking using it to retrieve the page and then resubmit.

The static method generateCSRFToken() will create a single-use token to place into a form to protect against CSRF attacks. When processing a form, the static method validateCSRFToken() should be used to ensure the form submission is valid by passing the $token as the first parameter. If the token is not valid, an fValidationException will be thrown.

The following HTML form and corresponding PHP will ensure that only users who request the form will then be able to delete their messages.

<form action="" method="post">
    <p>
        <input type="submit" value="Yes, delete my messages!" />
        <input type="hidden" name="request_token" value="<?php echo fRequest::generateCSRFToken() ?>" />
    </p>
</form>
if (fRequest::isPost()) {
    try {
        fRequest::validateCSRFToken(fRequest::get('request_token'));

        // Delete all of the users messages
        
    } catch (fExpectedException $e) {
        $e->printMessage();
    }
}

By default both generateCSRFToken() and validateCSRFToken() will use the current page as the identifier to use when checking the token. If one page is submitting the value, but another is checking it, the $url parameter of each method can be used.

<input type="hidden" name="request_token" value="<?php echo fRequest::generateCSRFToken('/processing_page.php') ?>" />
fRequest::validateCSRFToken(fRequest::get('request_token'), '/processing_page.php');

In addition, each URL can have multiple valid tokens at one time. This ensures that tabbed browsing will not cause the user to receive error messages about unauthenticated requests.

Checking The HTTP Method

When creating RESTful applications it is useful to know the HTTP method used to request the current page. There are four methods to check each of the four HTTP methods:

HTTP Method !fRequest Method
GET isGet()
POST isPost()
PUT isPut()
DELETE isDelete()
HEAD isHead()

Since browsers only currently support GET and POST method, isPost() will commonly be the only one used unless creating a RESTful API.

if (fRequest::isPost()) {
    // Perform constructive/destructive action
}

It is also possible to see if a request was made via AJAX by using the static method isAjax(). This checks the HTTP_X_REQUESTED_WITH HTTP header to see if it is set to xmlhttprequest.

if (fRequest::isAjax()) {
    // Perform partial interaction
}

Retrieving HTTP Accept Values

When building applications or sites that deliver different formats or translations of content, the HTTP Accept and Accept-Language headers include important information about what the client expects in response.

The Accept header includes a list of acceptable mime-types that the client expects in return. In addition, the HTTP spec includes a q value to indicate which mime-types are preferred over others. The Accept-Language header include a similar list for the language of the response. Accept-Language value can also include a q value for each language.

The static methods getAcceptTypes() and getAcceptLanguages() will each return a list of the acceptable mime-types and languages, respectively, ordered by the q values for each.

$mime_types = fRequest::getAcceptTypes();
$languages  = fRequest::getAcceptLanguages();

If no accepts headers are sent by the client, an empty array will be returned.

It is also possible to provide a list of valid accept types or languages and pick the clients preferred one by using the methods getBestAcceptType() and getBestAcceptLanguage().

Each method accepts a single parameter, an ordered array of the mime type or languages supported by your application. The type or language with the highest q value for the client will be returned.

$mime_type = fRequest::getBestAcceptType(array(
    'application/json',
    'text/html'
));
$language = fRequest::getBestAcceptLanguage(array(
    'en-us',
    'fr-ca'
));

If no value in the array matches, FALSE will be returned. If any value is acceptable, the first value in the array will be returned.

If no array of valid values is specified, and the client accepts any value, NULL will be returned. Otherwise the value specified by the client with the highest q will be returned.

Multiple Submit Buttons

There are sure to be times when a user is given multiple options when submitting a form and where you dont want to require the user to have javascript enabled in their browser. The overrideAction() method allows for a form to have multiple submit buttons and for them to trigger different functionality.

This solution stems from the problem that the only way to tell which submit button was click is that buttons name and value pair is added to the GET or POST data, while all other submit buttons are ignored. Rather than requiring the developer to manually check for specific values when creating multiple submit buttons, overrideAction() allows the name of a submit button to be set to action::{action_to_trigger}.

If a submit button with a name as above is clicked and overrideAction() is called in the destination page, the action parameter of $_GET or $_POST will all be overridden with this action.

Here is an example:

<form action="<?php echo fURL::get() ?>" method="post">
    ...
    <p>
        <input type="hidden" name="action" value="delete" />
        <input type="submit" value="Yes, delete this user" />
        <input type="submit" name="action::list" value="No, keep this user" />
    </p>
</form>

And the PHP to handle the form submission:

// We will call overrideAction() as the very first thing so everything else see the modified `$_POST` superglobal
fRequest::overrideAction();

// Get the action to execute
$action = fRequest::get('action');

If the user were to click Yes, delete this user then the action delete would be executed, however if the user clicked No, keep this user then the list action would be executed.

It is also possible to pass a redirection URL to overrideAction(). If the string %action% is found in the redirection URL, it will be replaced with the overridden action. Here is an example:

// Redirect the user to /admin/users/{action} if an overridden action is posted
fRequest::overrideAction('/admin/users/%action%');