*/ /** * Stores the path to the element's folder - needed to include the subclasses */ //define('PATFORMS_ELEMENT_DATE_INSTALL_DIR', WBParam::get('wb/dir/system') . '/include/patForms/patForms/Element'); /** * Notice: could not use the date set as default for the element, as it does not match * the specified date format string. */ define( 'PATFORMS_ELEMENT_DATE_NOTICE_DATE_PARSE_ERROR', 'patForms:Element:Date:01' ); /** * Notice: the date string ended unexpectedly - it is too short according to the format. */ define( 'PATFORMS_ELEMENT_DATE_NOTICE_DATE_UNEXPECTED_END', 'patForms:Element:Date:02' ); /** * Error: the specified date string is not a valid strtotime date string. */ define( 'PATFORMS_ELEMENT_DATE_ERROR_INVALID_DATE_DEFINITION', 'patForms:Element:Date:04' ); /** * Error: the PEAR_Calendar class is not installed */ define( 'PATFORMS_ELEMENT_DATE_WARNING_CALENDAR_NOT_INSTALLED', 'patForms:Element:Date:05' ); WBClass::load('Date' ,'patForms_Element_Date_Element' ); /** * patForms Date element * * Powerful date element with a very flexible date parser that enables * very intuitive usage of date input uses. * * $Id$ * * @access public * @package patForms * @subpackage Element * @author Sebastian 'The Argh' Mordziol * @license LGPL */ class patForms_Element_Date extends patForms_Element { /** * * @var DateTimeZone */ static private $timezone; public $locale = 'C'; /** * Stores the name of the element - this is used mainly by the patForms * error management and should be set in every element class. * @access public */ public $elementName = 'Date'; /** * set here which attributes you want to include in the element if you want to use * the {@link patForms_Element::convertDefinition2Attributes()} method to automatically * convert the values from your element definition into element attributes. * * @access protected * @see patForms_Element::convertDefinition2Attribute() */ public $attributeDefinition = array( 'id' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), ), 'name' => array( 'required' => true, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'type' => array( 'required' => false, 'format' => 'string', 'default' => 'text', 'outputFormats' => array( 'html' ), ), 'title' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'description' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array(), 'modifiers' => array( 'insertSpecials' => array() ), ), 'default' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array(), ), 'label' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array(), ), 'edit' => array( 'required' => false, 'format' => 'string', 'default' => 'yes', 'outputFormats' => array(), ), 'display' => array( 'required' => false, 'format' => 'string', 'default' => 'yes', 'outputFormats' => array(), ), 'required' => array( 'required' => false, 'format' => 'string', 'default' => 'yes', 'outputFormats' => array(), ), 'value' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), ), 'style' => array( 'required' => false, 'outputFormats' => array( 'html' ), 'format' => 'string', ), 'class' => array( 'required' => false, 'outputFormats' => array( 'html' ), 'format' => 'string', ), 'classtable' => array( 'required' => false, 'format' => 'string', 'default' => '', 'outputFormats' => array() ), 'autocomplete' => array( 'required' => false, 'format' => 'string', 'default' => '', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ) ), 'onchange' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'onclick' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'onfocus' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'onmouseover' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'onmouseout' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'onblur' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), 'modifiers' => array( 'insertSpecials' => array() ), ), 'accesskey' => array( 'required' => false, 'format' => 'string', 'outputFormats' => array( 'html' ), ), 'position' => array( 'required' => false, 'format' => 'int', 'outputFormats' => array(), ), 'tabindex' => array( 'required' => false, 'format' => 'int', 'outputFormats' => array( 'html' ), ), 'max' => array( 'required' => false, 'format' => 'datetime', 'outputFormats' => array(), ), 'min' => array( 'required' => false, 'format' => 'datetime', 'outputFormats' => array(), ), 'format' => array( 'required' => false, 'format' => 'datetime', 'outputFormats' => array(), ), 'dateformat' => array( 'required' => false, 'format' => 'string', 'default' => 'Y.m.d H:i:s', 'outputFormats' => array(), ), 'presets' => array( 'required' => false, 'format' => 'string', 'default' => 'yes', 'outputFormats' => array(), ), 'disabled' => array( 'required' => false, 'format' => 'string', 'default' => 'no', 'outputFormats' => array( 'html' ), ), 'returnformat' => array( 'required' => false, 'format' => 'string', 'default' => 'isodate', 'outputFormats' => array(), ), ); /** * the type of the element - set this to the type of element you are creating * if you want to use the {@link patForms_Element::element2html()} method to * create the final HTML tag for your element. * * @access public * @see patForms_Element::element2html() */ public $elementType = array( "html" => "input" ); /** * Stores a subelement counter used for the automatic ID generation for each of * the date element's sub elements. * * @access private * @var int */ public $elCount = 0; /** * Stores an index of all date tokens the date element supports, and the * corresponding subelement that handles the token. * * @access private * @var array * @see $tokens */ public $tokensIndex = array( 'Y' => 'Year', 'y' => 'Year', 'd' => 'Day', 'j' => 'Day', 'F' => 'Month', 'm' => 'Month', 'M' => 'Month', 'n' => 'Month', 'i' => 'Minute', 'a' => 'Meridiem', 'A' => 'Meridiem', 'g' => 'Hour', 'G' => 'Hour', 'h' => 'Hour', 'H' => 'Hour', 's' => 'Second', ); /** * Stores a list of date tokens for which the min and max attributes * have to be set when in 'presets' mode. * * @access private * @var array */ public $minmaxTokens = array( 'Y', 'y' ); /** * Similar to the {@link $tokensIndex} property, only without the subelements * information - only the tokens, as an indexed array so it is easier to check * whether a token is available. * * @access private * @var array */ public $tokens = array(); /** * Stores a list of all tokens the current date format string uses. Needed by some * subelements like the Meridiem subelement to check whether the related tokens are * present. * * @access private * @var array * @see tokenUsed() */ public $usedTokens = array(); /** * Stores a collection of all the elements the current date format uses. Built on * startup by the _init() method. * * @access private * @var array * @see _init() */ public $dateElements = array(); /** * Stores all date element objects needed for the current date format string. Created * automatically on startup by the _init() method. * * @access private * @var array * @see _init() */ public $elements = array(); /** * Stores a list of characters contained in the date format string that will be transformed * when serializing to HTML to ensure the output stays correct. * * @access private * @var array */ public $transformTable = array( ' ' => ' ' ); /** * Stores a list of attributes that all subelements will inherit from the date element * * @access private * @var array */ public $inheritableAttributes = array( 'edit', 'display', 'required', 'style', 'class' ); /** * Stores an index of all elements for easy element name handling * * @access private * @var array */ public $elementsIndex = array(); /** * Stores the current date object (the value of the element) * * @access private * @var object */ public $date = null; /** * Stores the default date object * * @access private * @var object */ public $defaultDate = null; /** * Stores the max date object * * @access private * @var object */ public $maxDate = null; /** * Stores the min date object * * @access private * @var object */ public $minDate = null; /** * Stores attributes for which there are special setter methods * which need to be called. This is mainly for date attributes * that need to be converted to date objects internally. * * @access private * @var array */ public $setterAttribs = array( 'default', 'max', 'min' ); /** * Initialize validation codes using gettext * * @access protected * @return bool $success Always returns true. * @see $attributeDefaults */ public function loadValidatiorErrorCodes() { $this->validatorErrorCodes = array( 1 => patI18n::dgettext( 'patForms', 'This field is required, please complete it.' ), 2 => patI18n::dgettext( 'patForms', 'Value must be after \'[MINDATE]\'!' ), 3 => patI18n::dgettext( 'patForms', 'Value must be before \'[MAXDATE]\'!' ), 4 => patI18n::dgettext( 'patForms', 'Incorrect date format' ), 5 => patI18n::dgettext( 'patForms', 'This date does not exist' ) ); return true; } /** * Runs all needed tasks that the date element needs to run - this * includes parsing the date format string and generating the date * subelements collection. Also sets any default values and max/min * directives as required. * * @access private * @return bool $success Always returns true. Errors occurring on parsing the default date are ignored and the default date ignored as well in that case. * @see $dateElements * @see $elements */ public function _init() { parent::_init(); // presets mode? check the attribute collection if( $this->attributes['presets'] == 'yes' ) { $this->checkMinMaxAttribs(); } /* if ('date' == $this->attributes['type']) { return; } */ $this->tokens = array_keys( $this->tokensIndex ); // now parse the format string to create the // elements collection. This fills the dateElements // property as well as the elements property. $cnt = strlen( $this->attributes["dateformat"] ); for( $i=0; $i < $cnt; $i++ ) { $char = $this->attributes["dateformat"][$i]; if( in_array( $char, $this->tokens ) ){ $elementID = $this->tokensIndex[$char]; $this->elements[$elementID] = $this->createElement( $elementID ); $this->elements[$elementID]->setToken( $char ); $element = array( 'type' => 'token', 'elementID' => $elementID, ); if( !in_array( $char, $this->usedTokens ) ) { array_push( $this->usedTokens, $char ); } } else { $element = array( 'type' => 'cdata', 'content' => $char ); } array_push( $this->dateElements, $element ); } $this->elementsIndex = array_keys( $this->elements ); // inherit any inheritable attributes foreach( $this->inheritableAttributes as $attribute ) { if( !isset( $this->attributes[$attribute] ) ) { continue; } $this->inheritAttribute( $attribute, $this->attributes[$attribute] ); } // special attributes which have their own setter methods foreach( $this->setterAttribs as $attributeName ) { if( isset( $this->attributes[$attributeName] ) ) { $setterMethod = 'set'.ucfirst( $attributeName ); $this->$setterMethod( $this->attributes[$attributeName] ); } } // submitted state foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->setSubmitted( $this->submitted ); } foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->init(); } return true; } /** * Wrapper for the main element function, with added functionality to * inherit the submitted state to all subelements. * * @access public * @param bool $state True if it has been submitted, false otherwise (default). */ public function setSubmitted( $state ) { $this->submitted = $state; foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->setSubmitted( $this->submitted ); } } /** * set the element's namespace * * @static * @access public * @param string namespace * @return null */ public function setNamespace($namespace) { foreach($this->elementsIndex as $elementID) { $this->elements[$elementID]->setNamespace($namespace); } } /** * Some date tokens require the min and max attributes to be set when * in presets mode, as these dates are used to create the span with * which the date selectors are filled. This method checks if all is * set. * * @access private * @return mixed $success True if OK, a patError object otherwise */ public function checkMinMaxAttribs() { $found = false; foreach( $this->minmaxTokens as $minmaxToken ) { if( strstr( $this->attributes['dateformat'], $minmaxToken ) ) { $found = true; break; } } if( !$found ) { return true; } if( !isset( $this->attributes['min'] ) ) { if( !is_null( $this->defaultDate ) ) { $minDate = new Date( $this->defaultDate ); } else { $minDate = new Date(); } $this->attributes['min'] = $minDate->getDate(); } if( !isset( $this->attributes['max'] ) ) { if( !is_null( $this->defaultDate ) ) { $maxDate = new Date( $this->defaultDate ); } else { $maxDate = new Date(); } // add 5 years $maxDate->addSeconds( 157680000 ); $this->attributes['max'] = $maxDate->getDate(); } if (empty($this->defaultDate)) { return true; } $def = $this->defaultDate->getDate(DATE_FORMAT_UNIXTIME); $max = strtotime($this->attributes['max']); if ($max < $def) { $this->defaultDate = new Date($max); return true; } $min = strtotime($this->attributes['min']); if ($min > $def) { $this->defaultDate = new Date($min); return true; } return true; } public function setDefault( $date ) { $date = $this->addTimeZoneOffset($date); $this->defaultDate = $this->getDate( $date ); if( patErrorManager::isError( $this->defaultDate ) ) { return $this->defaultDate; } foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->setDefault( $this->defaultDate ); } } public function setMax( $date ) { $this->maxDate = $this->getDate( $date ); if( patErrorManager::isError( $this->maxDate ) ) { return $this->maxDate; } foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->setMax( $this->maxDate ); } } public function setMin( $date ) { $this->minDate = $this->getDate( $date ); if( patErrorManager::isError( $this->minDate ) ) { return $this->minDate; } foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->setMin( $this->minDate ); } } /** * Converts a strtotime-compatible date string that is used to define * the default date, as well as the max/min dates into a date string * according to the given format so the internal date parser can handle * it. * * @access private * @param string $dateElement A date string/object * @return mixed $date The converted, internal parsing engine compatible date string or a patError object if failed. * @todo Under windows, the strtotime public function does not return the time, only the date in its timestamp. Check a solution for this problem. */ public function getDate( $dateElement ) { if( !is_object( $dateElement ) ) { $date = new Date( $dateElement ); if( $date->getYear() == 0 ) { $stamp = strtotime( $dateElement ); if( $stamp == -1 ) { return patErrorManager::raiseError( PATFORMS_ELEMENT_DATE_ERROR_INVALID_DATE_DEFINITION, 'Invalid date', '['.$dateElement.'] seems not to be a valid date argument for the strtotime function. Please check the syntax.' ); } $date = new Date( $stamp ); } } // if there is no year, the date object could not convert the // date string to a valid date. /* if( $date->getYear() == 0 ) { return patErrorManager::raiseError( PATFORMS_ELEMENT_DATE_ERROR_INVALID_DATE_DEFINITION, 'Invalid date', '['.$dateElement.'] seems not to be a valid date. Please check your date syntax.' ); } */ return $date; } /** * Set value wrapper to adjuste timezone's offset * * @access public * @param string $value The date to set. Has to be a timestamp or strtotime compatible string */ public function setValue($value) { $value = $this->addTimeZoneOffset($value); $this->date = $this->getDate($value); if (patErrorManager::isError($this->date)) { return $this->date; } foreach ($this->elementsIndex as $elementID) { $this->elements[$elementID]->setValue($this->date); } } /** * mangle data before setDefaultValue() * * @access public * @param mixed $value The value to set * @see $value * @see resolveValue() * @see setValue() * @see getValue() */ public function setDefaultValue($value) { $value = $this->addTimeZoneOffset($value); parent::setDefaultValue($value); } /** * retrieves the current value of the element. If none is set, will try to retrieve the * value from submitted form data. * * @access public * @param boolean Determines whether the method is used from an external script * @return mixed The value, or an empty string if none found. * @see setValue() * @see value * @see resolveValue() */ public function getValue($external = true) { $value = parent::getValue($external); $value = $this->addTimeZoneOffset($value, false); return $value; } /** * adjust timestamp according to current timezone * * In case current Date-element is configured using isodates (attribute: returnformat) * and contains hours (attribute: dateformat), the timestamp will be adjusted according * locale config. * * In other words, this method automatically adds/substract time offset, in order to * display time select controls matching user's local time. * * @param string $value * @param bool $plus either to add or substract the timezone's offset * @return string adjusted timestamp */ private function addTimeZoneOffset($value, $plus = true) { if ('isodate' != $this->attributes['returnformat'] || !stristr($this->attributes['dateformat'], 'h')) { return $value; } if (!self::$timezone) { $conf = WBClass::create('WBConfig'); $conf->load('locale'); self::$timezone = new DateTimeZone($conf->get('locale/timezone', 'UTC')); } $ts = strtotime($value); $date = new DateTime($value, self::$timezone); if ($plus) { $ts += $date->getOffset(); } else { $ts -= $date->getOffset(); } $value = date('Y-m-d H:i:s', $ts); return $value; } /** * Checks whether a token has been used in the current date format string. * Used by some subelements to check whether the related tokens have been set * or not. * * @access private * @param string $token The token to check * @return bool $used True if used, false otherwise. * @see $usedTokens */ public function tokenUsed( $token ) { if( in_array( $token, $this->usedTokens ) ) { return true; } return false; } /** * Creates a date subelement from the corresponding class file, instantiates and * configures it, and returns it. * * Use WBClass to instatiate class * * @access private * @param string $role The role of the element, i.e. its name * @return object $element The requested element object */ private function createElement( $role ) { $el = WBClass::create('patForms_Element_Date_Element_'. $role); ++$this->elCount; $el->setID($this->elCount); $el->setParentName($this->attributes['name']); $el->setParentID($this->attributes['id']); $el->setParent($this); $locale = 'C'; if (class_exists('patI18n', false)) { $locale = patI18n::getLocale(); } $el->setLocale($locale); if ($this->attributes['presets'] == 'yes') { $el->setMode('presets'); } return $el; } /** * element creation method for the 'HTML' format in the 'default' form mode. * * @access public * @param mixed value of the element * @return mixed $element The element, or false if failed. */ public function serializeHtmlDefault( $value ) { // handle display attribute if( $this->attributes['display'] == 'no' ) { return $this->createDisplaylessTag( $value ); } $content = '' . ' '; foreach( $this->dateElements as $row => $element ) { $content .= '\n"; } $content .= ' ' . '
'; switch( $element['type'] ) { case 'token': $content .= $this->elements[$element['elementID']]->serialize(); break; default: if( isset( $this->transformTable[$element['content']] ) ) { $content .= $this->transformTable[$element['content']]; } else { $content .= $element['content']; } break; } $content .= "
'; return $content; } /** * Special implementation of the common method to gather the subelement's * displayless tags and use them as displayless content. * * @access private * @param mixed $value Unused - the value of the element * @return mixed $content The element content, or a patError object if failed. */ public function createDisplaylessTag( $value ) { $content = ''; $this->inheritAttribute( 'display', 'no' ); foreach( $this->elements as $elementID => $element ) { $serialized = $element->serialize(); if( patErrorManager::isError( $serialized ) ) { return $serialized; } $content .= $serialized; } return $content; } /** * Retrieves a format string for use with the date() function, * in the ISO 8601 format. Use this if you are too lazy to * remember it by yourself. * * @access public * @return string $formatString The format string */ public function getISOFormat() { return 'Y-m-d H:i:s'; } /** * Makes an attribute of the date element be inherited by the subelements. * * @access public * @param string $attribute The name of the attribute * @param mixed $value The value of the attribute */ public function inheritAttribute( $attribute, $value ) { if( !in_array( $attribute, $this->inheritableAttributes ) ) { return true; } foreach( $this->elementsIndex as $elementID ) { $this->elements[$elementID]->inheritAttribute( $attribute, $value ); } return true; } /** * element creation method for the 'HTML' format in the 'readonly' form mode. * Very simple; just returns the stored element value. * * @access public * @param mixed value of the element * @return string $value The element's value * @todo check wether the call to getAttributesFor() is needed */ public function serializeHtmlReadonly( $value ) { $tag = $this->createDisplaylessTag( $value ); // handle display attribute if( $this->attributes['display'] == 'no' ) { return $tag; } return $value.$tag; } /** * Retrieve a dte string according to the specified dateformat. Use * this if you need the selected date in that format, which is * otherwise solely used for layout. * * @access public * @return string $formattedDate The formatted date */ public function getFormattedDate() { // resolve the current date $this->resolveValue(); return date( $this->attributes['dateformat'], $this->date->getTime() ); } /** * resolves the scope the value of the element may be stored in, and returns it. * * @access protected * @see getValue() * @see value * @todo parse element name, if it uses the array syntax */ public function resolveValue() { $returnformat = 'isodate'; if( $this->attributes['returnformat'] != $returnformat ) { $returnformat = $this->attributes['returnformat']; } // resolve date object to use if( is_null( $this->date ) ) { if( !is_null( $this->defaultDate ) ) { $this->date = $this->defaultDate; } else { $this->date = new Date(); } } $valid = true; // now set the needed parts of the date element foreach( $this->elementsIndex as $elementID ) { $methodName = $this->elements[$elementID]->setterMethod(); $value = $this->elements[$elementID]->getValue(); if( is_null( $value ) ) { $valid = false; continue; } $this->date->$methodName( $value ); } if( !$valid ) { $this->value = null; return true; } switch( $returnformat ) { case 'timestamp': $this->value = $this->date->getTime(); break; case 'isoday': $this->value = substr($this->date->getDate(), 0, 10); break; case 'isodate': default: $this->value = $this->date->getDate(); break; } $this->value = $this->_applyFilters( $this->value, 'in', PATFORMS_FILTER_TYPE_PHP ); return true; } /** * validates the element. * * @access public * @param mixed value of the element * @return bool $isValid True if element could be validated, false otherwise. * @todo The required check should go through each subelement and generate an error message according to the subelement(s) that have errors */ public function validateElement( $value ) { $required = false; // store the required flag for easy access if( $this->attributes['required'] == "yes" ) { $required = true; } // nothing specified and element is not required, we're done. if( !$required && is_null( $value ) ) { return true; } // nothing specified and element is required, we need some data. if( $required && is_null( $value ) ) { $this->addValidationError( 1 ); return false; } foreach( $this->elementsIndex as $elementID ) { if( !$this->elements[$elementID]->validate() ) { $this->addValidationError( 4 ); return false; } } if( !is_null( $this->minDate ) && $this->date->before( $this->minDate ) ) { $this->addValidationError( 2, array( 'mindate' => $this->minDate->getDate() ) ); } if( !is_null( $this->maxDate ) && $this->date->after( $this->maxDate ) ) { $this->addValidationError( 3, array( 'maxdate' => $this->maxDate->getDate() ) ); } $date = $this->getDate( $value ); $adjusted = date( 'Y-m-d H:i:s', mktime( $date->getHour(), $date->getMinute(), $date->getSecond(), $date->getMonth(), $date->getDay(), $date->getYear() ) ); if( $adjusted != $date->getDate() ) { $this->addValidationError( 5 ); return false; } // validate input return true; } /** * Wrapper for the normal element setAttribute method, which handles * inheriting the attributes to all its children * * @access public * @param string $attributeName The name of the attribute to add * @param string $attributeValue The value of the attribute * @return mixed $success True on success, a patError object otherwise */ public function setAttribute( $attributeName, $attributeValue ) { // set the attribute normally $success = parent::setAttribute( $attributeName, $attributeValue ); if( patErrorManager::isError( $success ) ) { return $success; } // special attributes that have their own setter method if( in_array( $attributeName, $this->setterAttribs ) ) { $setterMethod = 'set'.ucfirst( $attributeName ); $this->$setterMethod( $attributeValue ); } return $this->inheritAttribute( $attributeName, $attributeValue ); } }