blob: 89385036c1ab7634154839b37021bd051de8de97 [file] [log] [blame]
<?php
/**
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @version //autogentag//
* @filesource
* @package UserInput
*/
/**
* Provides access to form variables.
*
* This class allows you to retrieve input variables from the request in a safe
* way, by applying filters to allow only wanted data into your application. It
* works by passing an array that describes your form definition to the
* constructor of the class. The constructor will then initialize the class
* with properties that contain the value of your request's input fields.
*
* Example:
* <code>
* <?php
* if ( ezcInputForm::hasGetData() )
* {
* $definition = array(
* 'fieldname' => new ezcInputFormDefinitionElement(
* ezcInputFormDefinitionElement::REQUIRED, 'filtername'
* ),
* 'textfield' => new ezcInputFormDefinitionElement(
* ezcInputFormDefinitionElement::OPTIONAL, 'string'
* ),
* 'integer1' => new ezcInputFormDefinitionElement(
* ezcInputFormDefinitionElement::REQUIRED, 'int',
* array( 'min_range' => 0, 'max_range' => 42 )
* ),
* 'xmlfield' => new ezcInputFormDefinitionElement(
* ezcInputFormDefinitionElement::REQUIRED, 'unsafe_raw'
* ),
* 'special' => new ezcInputFormDefinitionElement(
* ezcInputFormDefinitionElement::OPTIONAL, 'callback',
* array( 'ezcInputFilter', 'special' )
* ),
* );
* $form = new ezcInputForm( INPUT_GET, $definition );
* if ( $form->hasInputField( 'textfield' ) ) // check for optional field
* {
* $text = $form->textfield;
* }
*
* try
* {
* $xml = $form->xmlfield; // Uses dynamic properties through __get().
* $field = $form->fieldname;
* $int = $form->integer1;
* }
* catch ( ezcInputFormException $e )
* {
* // one of the required fields didn't have valid data.
* $invalidProperties = $form->getInvalidProperties();
*
* // Retrieve RAW data for invalid properties so that we can fill in the
* // forms online with this RAW data again - Make sure to escape it on
* // output though, but that should be done for all data anyway.
* if ( in_array( 'xmlfield', $invalidProperties ) )
* {
* $xml = $form->getUnsafeRawData( 'xmlfield' );
* }
* }
*
* // Checking optional fields
* foreach ( $form->getOptionalProperties() as $property )
* {
* $name = "property_{$property}";
* if ( $form->hasValidData( $property ) )
* {
* $$name = $form->$property;
* }
* }
* }
* ?>
* </code>
*
* @property-read string $formFields
* There is a read-only property for each field that is defined
* as input field.
*
* @package UserInput
* @version //autogentag//
* @mainclass
*/
class ezcInputForm
{
/**
* @var VALID is used in the $properties array to record whether the data
* in a specific input variable contained valid data according
* to the filter.
*/
const VALID = 0;
/**
* @var INVALID is used in the $properties array to record whether the data
* in a specific input variable contained valid data according
* to the filter.
*/
const INVALID = 1;
const DEF_NO_ARRAY = 1;
const DEF_EMPTY = 2;
const DEF_ELEMENT_NO_DEFINITION_ELEMENT = 3;
const DEF_NOT_REQUIRED_OR_OPTIONAL = 5;
const DEF_WRONG_FLAGS_TYPE = 6;
const DEF_UNSUPPORTED_FILTER = 7;
const DEF_FIELD_NAME_BROKEN = 8;
/**
* Contains the definition for this form (as passed in the constructor).
* @var array(string=>ezcInputFormDefinitionElement)
*/
private $definition;
/**
* Contains a list of all retrieved properties and their status. The key
* for each array element is the field name, and the value associated with
* this key is one of the constants VALID or INVALID.
* @var array
*/
private $properties;
/**
* Contains the values of the input variables. The key for each array
* element is the field name, and the value associated with this key is the
* property's value. This array does not have an entry for input fields
* that do not have valid data.
* @var array
*/
protected $propertyValues;
/**
* Contains the input source to be used.
* @var int
*/
private $inputSource;
/**
* Whether all the input elements are valid
*/
private $allElementsValid;
/**
* Constructs a new ezcInputForm for $inputSource with $definition.
*
* This method constructs a new ezcInputForm with three parameters. The
* $inputSource parameter selects the input source and should be one of the
* constants INPUT_GET, INPUT_POST or INPUT_COOKIE. The $definition
* parameter is an array of ezcInputFormDefinitionElement items and
* determines which input variables make up this form (see the example at
* the top of this class). The last parameter, $characterEncoding is the
* character encoding to use while retrieving input variable data. This
* parameter has currently no function as it will depend on PHP 6
* functionality which does not exist yet in the input filter extension.
*
* @throws ezcInputFormVariableMissingException when one of the required
* input variables is missing.
* @throws ezcInputFormInvalidDefinitionException when the definition array
* is invalid or when the input source was invalid.
*
* @param int $inputSource
* @param array(ezcInputFormDefinitionElement) $definition
* @param string $characterEncoding
*/
public function __construct( $inputSource, $definition, $characterEncoding = null )
{
if ( ( $returnValue = ezcInputForm::validateDefinition( $definition ) ) !== true )
{
throw new ezcInputFormInvalidDefinitionException( $returnValue[1] );
}
$this->definition = $definition;
$this->inputSource = $inputSource;
$this->parseInput();
}
/**
* Returns whether there is GET data available
*
* @return bool True if there is GET data available
*/
static public function hasGetData()
{
return count( $_GET ) > 0;
}
/**
* Returns whether there is POST data available
*
* @return bool True if there is POST data available
*/
static public function hasPostData()
{
return count( $_POST ) > 0;
}
/**
* Parses the input according to the definition array.
*
* @throws ezcInputFormInvalidDefinitionException when one of the required
* input variables is missing or when the input source was invalid.
*/
private function parseInput()
{
$this->allElementsValid = true;
if ( !in_array( $this->inputSource, array( INPUT_GET, INPUT_POST, INPUT_COOKIE ) ) )
{
throw new ezcInputFormWrongInputSourceException( $this->inputSource );
}
foreach ( $this->definition as $elementName => $inputElement )
{
$hasVariable = filter_has_var( $this->inputSource, $elementName );
if ( ! $hasVariable )
{
if ( $inputElement->type === ezcInputFormDefinitionElement::REQUIRED )
{
throw new ezcInputFormVariableMissingException( $elementName );
}
else
{
$this->properties[$elementName] = ezcInputForm::INVALID;
$this->allElementsValid = false;
continue;
}
}
$flags = FILTER_NULL_ON_FAILURE | $inputElement->flags;
$value = filter_input( $this->inputSource, $elementName, filter_id( $inputElement->filterName ), array( 'options' => $inputElement->options, 'flags' => $flags ) );
if ( $value !== null )
{
$this->properties[$elementName] = ezcInputForm::VALID;
$this->propertyValues[$elementName] = $value;
}
else
{
$this->properties[$elementName] = ezcInputForm::INVALID;
$this->allElementsValid = false;
}
}
}
/**
* Validates the definition array $definition.
*
* Before reading the values from the input source, the definition array
* can be validated by this method to check whether all necessary
* elements are correctly formed.
*
* With the following code you can check whether the definition is valid:
* <code>
* <?php
* if ( ( $returnValue = ezcInputForm::validateDefinition( $definition ) ) !== true )
* {
* // do something with the error type and error message in $returnValue
* }
* else
* {
* // the definition was correct
* }
* ?>
* </code>
*
* @param array $definition
* @return array|bool If the definition is correct the method returns
* boolean true. When an error is found the function
* returns an array where the first element is the type,
* and the second element the error message.
*/
static public function validateDefinition( $definition )
{
// The definition parameter should be an array
if ( !is_array( $definition ) )
{
return array( ezcInputForm::DEF_NO_ARRAY, "The definition array is not an array" );
}
// There should be atleast one element
if ( count( $definition ) === 0 )
{
return array( ezcInputForm::DEF_EMPTY, "The definition array is empty" );
}
foreach ( $definition as $name => $element )
{
// Each element should be an ezcInputFormDefinitionElement
if ( !$element instanceof ezcInputFormDefinitionElement )
{
return array( ezcInputForm::DEF_ELEMENT_NO_DEFINITION_ELEMENT, "The definition for element '{$name}' is not an ezcInputFormDefinitionElement" );
}
// The first value in an element should be REQUIRED or OPTIONAL
if ( !in_array( $element->type, array( ezcInputFormDefinitionElement::OPTIONAL, ezcInputFormDefinitionElement::REQUIRED ), true ) )
{
return array( ezcInputForm::DEF_NOT_REQUIRED_OR_OPTIONAL, "The first element definition for element '{$name}' is not ezcInputFormDefinitionElement::OPTIONAL or ezcInputFormDefinitionElement::REQUIRED" );
}
// The options should either be an array, a string, or an int
if ( $element->options !== null )
{
$filterOptionsType = gettype( $element->options );
if ( !in_array( $filterOptionsType, array( 'integer', 'string', 'array' ) ) )
{
return array( ezcInputForm::DEF_WRONG_FLAGS_TYPE, "The options to the definition for element '{$name}' is not of type integer, string or array" );
}
// A callback filter should have the form "string" or "array(string, string)"
if ( $element->filterName == 'callback' )
{
if ( $filterOptionsType == 'integer' )
{
return array( ezcInputForm::DEF_WRONG_FLAGS_TYPE, "The callback filter for element '{$name}' should not be an integer" );
}
if ( $filterOptionsType == 'array' )
{
if ( count( $element->options ) != 2 )
{
return array( ezcInputForm::DEF_WRONG_FLAGS_TYPE, "The array parameter for the callback filter for element '{$name}' should have exactly two elements" );
}
if ( gettype( $element->options[0] ) != 'string' || gettype( $element->options[1] ) != 'string' )
{
return array( ezcInputForm::DEF_WRONG_FLAGS_TYPE, "The array elements for the callback filter for element '{$name}' should both be a string" );
}
}
}
}
// The options should either be an int
if ( $element->flags !== null )
{
if ( gettype( $element->flags ) !== 'integer' )
{
return array( ezcInputForm::DEF_WRONG_FLAGS_TYPE, "The flags to the definition for element '{$name}' is not of type integer, string or array" );
}
}
// The filter should be an existing filter
if ( !in_array( $element->filterName, filter_list() ) )
{
$filters = join( ', ', filter_list() );
return array( ezcInputForm::DEF_UNSUPPORTED_FILTER, "The filter '{$element->filterName}' for element '{$name}' does not exist. Pick one of: $filters" );
}
// The input field name should have a sane format
if ( gettype( $name ) != 'string' )
{
return array( ezcInputForm::DEF_FIELD_NAME_BROKEN, "The element name '{$name}' is not a string" );
}
if (! preg_match( '@^[a-z][a-z0-9_]*$@i', $name ) )
{
return array( ezcInputForm::DEF_FIELD_NAME_BROKEN, "The element name '{$name}' has an unsupported format. It should start with an a-z and followed by a-z0-9_" );
}
}
return true;
}
/**
* This function is called when a variable is assigned to a magic property.
*
* When the value of a property is requested this function checks with the
* $properties array whether it contains valid data or not. If there is no
* valid data, the UserInputInValidData exception is thrown, otherwise the
* function returns the value associated with the input variable.
*
* @throws ezcInputFormInvalidDataException when trying to read a property
* which has no valid data.
* @throws ezcInputFormUnknownFieldException when a property is being
* accessed which is not defined in the definition array.
*
* @param string $propertyName
* @return mixed The value of the input variable.
* @ignore
*/
public function __get( $propertyName )
{
if ( isset( $this->properties[$propertyName] ) )
{
if ( $this->properties[$propertyName] === ezcInputForm::VALID )
{
return $this->propertyValues[$propertyName];
}
else
{
throw new ezcInputFormNoValidDataException( $propertyName );
}
}
throw new ezcInputFormUnknownFieldException( $propertyName );
}
/**
* Returns whether a magic property was is used on a magic property.
*
* This method checks whether a magic property exists and returns true of
* it does and false if it doesn't. The list of properties which exist is
* determined by the $definition array that was passed to the constructor.
*
* @param string $propertyName
* @return bool Whether the $propertyName exists or not.
* @ignore
*/
public function __isset( $propertyName )
{
return isset( $this->properties[$propertyName] );
}
/**
* Sets a new magic property.
*
* This function is called when one of the magic properties was assigned a
* new value. As all magic properties are read-only for this class, all
* that this function does is return the exception
* ezcBasePropertyReadOnlyException.
*
* @throws ezcBasePropertyPermissionException for every call to this
* function.
* @param string $propertyName
* @param mixed $newValue
* @ignore
*/
public function __set( $propertyName, $newValue )
{
throw new ezcBasePropertyPermissionException( $propertyName, ezcBasePropertyPermissionException::READ );
}
/**
* Returns whether the optional field $fieldName exists.
*
* @param string $fieldName
* @return bool true if the input field was available and false otherwise.
*/
public function hasInputField( $fieldName )
{
if ( isset( $this->properties[$fieldName] ) )
{
return true;
}
return false;
}
/**
* Returns whether the filters for required field $fieldName returned valid data.
*
* @param string $fieldName
* @return bool true if the input field was available and false otherwise.
*/
public function hasValidData( $fieldName )
{
if ( isset( $this->properties[$fieldName] ) && $this->properties[$fieldName] === ezcInputForm::VALID )
{
return true;
}
return false;
}
/**
* Returns RAW input variable values for invalid field $fieldName.
*
* The return value of this function can be used to prefill forms on the
* next request. It will only work for invalid input fields, as for valid
* input fields you should never have to get to the original RAW data. In
* the case a $fieldName is passed that has valid data, an
* ezcInputFormException will be thrown.
*
* @throws ezcInputFormValidDataException when trying to get unsafe raw
* data from a input field with valid data.
* @throws ezcInputFormFieldNotFoundException when trying to get data from a
* property that does not exist.
* @param string $fieldName
* @return string The original RAW data of the specified input field.
*/
public function getUnsafeRawData( $fieldName )
{
if ( isset( $this->properties[$fieldName] ) )
{
if ( $this->properties[$fieldName] === ezcInputForm::VALID )
{
throw new ezcInputFormValidDataAvailableException( $fieldName );
}
else
{
if ( filter_has_var( $this->inputSource, $fieldName ) )
{
return filter_input( $this->inputSource, $fieldName, FILTER_UNSAFE_RAW );
}
else
{
throw new ezcInputFormFieldNotFoundException( $fieldName );
}
}
}
throw new ezcInputFormUnknownFieldException( $fieldName );
}
/**
* Returns a list with all optional properties.
* @return array(string)
*/
public function getOptionalProperties()
{
$properties = array();
foreach ( $this->definition as $fieldName => $fieldDefinition )
{
if ( $fieldDefinition->type === ezcInputFormDefinitionElement::OPTIONAL )
{
$properties[] = $fieldName;
}
}
return $properties;
}
/**
* Returns a list with all required properties.
* @return array(string)
*/
public function getRequiredProperties()
{
$properties = array();
foreach ( $this->definition as $fieldName => $fieldDefinition )
{
if ( $fieldDefinition->type === ezcInputFormDefinitionElement::REQUIRED )
{
$properties[] = $fieldName;
}
}
return $properties;
}
/**
* Returns a list with all properties that have valid data.
* @return array(string)
*/
public function getValidProperties()
{
$properties = array();
foreach ( $this->properties as $fieldName => $fieldStatus )
{
if ( $fieldStatus === ezcInputForm::VALID )
{
$properties[] = $fieldName;
}
}
return $properties;
}
/**
* Returns a list with all properties having invalid data.
* @return array(string)
*/
public function getInvalidProperties()
{
$properties = array();
foreach ( $this->properties as $fieldName => $fieldStatus )
{
if ( $fieldStatus === ezcInputForm::INVALID )
{
$properties[] = $fieldName;
}
}
return $properties;
}
/**
* Returns whether all the input elements were valid or not.
*
* @return bool
*/
public function isValid()
{
return $this->allElementsValid;
}
}
?>