symphony/lib/toolkit/fields/field.date.php
<?php
/**
* @package toolkit
*/
/**
* A simple Date field that stores a full ISO date. Symphony will attempt
* to localize the date on a per Author basis. The field essentially maps to
* PHP's `strtotime`, so it is very flexible in terms of what an Author can
* input into it.
*/
class FieldDate extends Field implements ExportableField, ImportableField
{
/**
* @deprecated @since Symphony 3.0.0
*/
const SIMPLE = 0;
/**
* @deprecated @since Symphony 3.0.0
*/
const REGEXP = 1;
/**
* @deprecated @since Symphony 3.0.0
*/
const RANGE = 3;
/**
* @deprecated @since Symphony 3.0.0
*/
const ERROR = 4;
/**
* @deprecated @since Symphony 3.0.0
*/
private $key;
/**
* @deprecated @since Symphony 3.0.0
*/
protected static $min_date = '1000-01-01 00:00:00';
/**
* @deprecated @since Symphony 3.0.0
*/
protected static $max_date = '9999-12-31 23:59:59';
public function __construct()
{
parent::__construct();
$this->_name = __('Date');
$this->_required = true;
$this->key = 1;
$this->entryQueryFieldAdapter = new EntryQueryDateAdapter($this);
$this->set('pre_populate', 'now');
$this->set('required', 'no');
$this->set('location', 'sidebar');
}
/*-------------------------------------------------------------------------
Definition:
-------------------------------------------------------------------------*/
public function canFilter()
{
return true;
}
public function isSortable()
{
return true;
}
public function canPrePopulate()
{
return true;
}
public function allowDatasourceOutputGrouping()
{
return true;
}
public function allowDatasourceParamOutput()
{
return true;
}
public function fetchFilterableOperators()
{
return array(
array(
'title' => 'is',
'filter' => ' ',
'help' => __('Find values that are an exact match for the given string.')
),
array(
'filter' => 'sql: NOT NULL',
'title' => 'is not empty',
'help' => __('Find entries where any value is selected.')
),
array(
'filter' => 'sql: NULL',
'title' => 'is empty',
'help' => __('Find entries where no value is selected.')
),
array(
'title' => 'contains',
'filter' => 'regexp: ',
'help' => __('Find values that match the given <a href="%s">MySQL regular expressions</a>.', array(
'https://dev.mysql.com/doc/mysql/en/regexp.html'
))
),
array(
'title' => 'does not contain',
'filter' => 'not-regexp: ',
'help' => __('Find values that do not match the given <a href="%s">MySQL regular expressions</a>.', array(
'https://dev.mysql.com/doc/mysql/en/regexp.html'
))
),
array(
'title' => 'later than',
'filter' => 'later than '
),
array(
'title' => 'earlier than',
'filter' => 'earlier than '
),
array(
'title' => 'equal to or later than',
'filter' => 'equal to or later than '
),
array(
'title' => 'equal to or earlier than',
'filter' => 'equal to or earlier than '
),
);
}
public function fetchSuggestionTypes()
{
return array('date');
}
/*-------------------------------------------------------------------------
Setup:
-------------------------------------------------------------------------*/
public function createTable()
{
return Symphony::Database()
->create('tbl_entries_data_' . General::intval($this->get('id')))
->ifNotExists()
->fields([
'id' => [
'type' => 'int(11)',
'auto' => true,
],
'entry_id' => 'int(11)',
'value' => [
'type' => 'varchar(80)',
'null' => true,
],
])
->keys([
'id' => 'primary',
'entry_id' => 'unique',
'value' => 'key',
])
->execute()
->success();
}
/*-------------------------------------------------------------------------
Utilities:
-------------------------------------------------------------------------*/
/**
* Given a string, this function builds the range of dates that match it.
* The strings should be in ISO8601 style format, or a natural date, such
* as 'last week' etc.
*
* @deprecated @since Symphony 3.0.0
* Use DateRangeParser class instead
* @since Symphony 2.2.2
* @param array $string
* The date string to be parsed
* @param string $direction
* Either later or earlier, defaults to null.
* @param boolean $equal_to
* If the filter is equal_to or not, defaults to false.
* @return array
* An associative array containing a date in ISO8601 format (or natural)
* with two keys, start and end.
*/
public static function parseDate($string, $direction = null, $equal_to = false)
{
$parts = array(
'start' => null,
'end' => null
);
// Year
if (preg_match('/^\d{1,4}$/', $string, $matches)) {
$year = current($matches);
$parts['start'] = "$year-01-01 00:00:00";
$parts['end'] = "$year-12-31 23:59:59";
$parts = self::isEqualTo($parts, $direction, $equal_to);
// Year/Month/Day/Time
} elseif (preg_match('/^\d{1,4}[-\/]\d{1,2}[-\/]\d{1,2}\s\d{1,2}:\d{2}/', $string, $matches)) {
// Handles the case of `to` filters
if ($equal_to || is_null($direction)) {
$parts['start'] = $parts['end'] = DateTimeObj::get('Y-m-d H:i:s', $string);
} else {
$parts['start'] = DateTimeObj::get('Y-m-d H:i:s', $string . ' - 1 second');
$parts['end'] = DateTimeObj::get('Y-m-d H:i:s', $string . ' + 1 second');
}
// Year/Month/Day
} elseif (preg_match('/^\d{1,4}[-\/]\d{1,2}[-\/]\d{1,2}$/', $string, $matches)) {
$year_month_day = current($matches);
$parts['start'] = "$year_month_day 00:00:00";
$parts['end'] = "$year_month_day 23:59:59";
$parts = self::isEqualTo($parts, $direction, $equal_to);
// Year/Month
} elseif (preg_match('/^\d{1,4}[-\/]\d{1,2}$/', $string, $matches)) {
$year_month = current($matches);
$parts['start'] = "$year_month-01 00:00:00";
$parts['end'] = DateTimeObj::get('Y-m-t', $parts['start']) . " 23:59:59";
$parts = self::isEqualTo($parts, $direction, $equal_to);
// Relative date, aka '+ 3 weeks'
} else {
// Handles the case of `to` filters
if ($equal_to || is_null($direction)) {
$parts['start'] = $parts['end'] = DateTimeObj::get('Y-m-d H:i:s', $string);
} else {
$parts['start'] = DateTimeObj::get('Y-m-d H:i:s', $string . ' - 1 second');
$parts['end'] = DateTimeObj::get('Y-m-d H:i:s', $string . ' + 1 second');
}
}
return $parts;
}
/**
* Builds the correct date array depending if the filter should include
* the filter as well, ie. later than 2011, is effectively the same as
* equal to or later than 2012.
*
* @deprecated @since Symphony 3.0.0
* Use DateRangeParser class instead
* @since Symphony 2.2.2
* @param array $parts
* An associative array containing a date in ISO8601 format (or natural)
* with two keys, start and end.
* @param string $direction
* Either later or earlier, defaults to null.
* @param boolean $equal_to
* If the filter is equal_to or not, defaults to false.
* @return array
*/
public static function isEqualTo(array $parts, $direction, $equal_to = false)
{
if (!$equal_to) {
return $parts;
}
if ($direction == 'later') {
$parts['end'] = $parts['start'];
} else {
$parts['start'] = $parts['end'];
}
return $parts;
}
/**
* @deprecated @since Symphony 3.0.0
* Use EntryQueryAuthorAdapter class instead
*
* @param string $string
* @return int
*/
public static function parseFilter(&$string)
{
$string = self::cleanFilterString($string);
// Relative check, earlier or later
if (preg_match('/^(equal to or )?(earlier|later) than (.*)$/i', $string, $match)) {
$string = $match[3];
// Validate date
if (!DateTimeObj::validate($string)) {
return self::ERROR;
}
// Date is equal to or earlier/later than
// Date is earlier/later than
$parts = self::parseDate($string, $match[2], $match[1] == "equal to or ");
$earlier = $parts['start'];
$later = $parts['end'];
// Switch between earlier than and later than logic
// The earlier/later range is defined by MySQL's support. RE: #1560
// @link https://dev.mysql.com/doc/refman/en/datetime.html
switch ($match[2]) {
case 'later':
$string = $later . ' to ' . self::$max_date;
break;
case 'earlier':
$string = self::$min_date . ' to ' . $earlier;
break;
}
} elseif (preg_match('/IS( NOT)? NULL/i', $string)) {
$string = array('start' => $string, 'end' => $string);
return self::RANGE;
// Look to see if its a shorthand date (year only), and convert to full date
// Look to see if the give date is a shorthand date (year and month) and convert it to full date
// Match single dates
} elseif (
preg_match('/^(1|2)\d{3}$/i', $string)
|| preg_match('/^(1|2)\d{3}[-\/]\d{1,2}$/i', $string)
|| !preg_match('/\s+to\s+/i', $string)
) {
// Validate
if (!DateTimeObj::validate($string)) {
return self::ERROR;
}
$parts = self::parseDate($string);
$string = $parts['start'] . ' to ' . $parts['end'];
// Match date ranges
} elseif (preg_match('/\s+to\s+/i', $string)) {
if (!$parts = preg_split('/\s+to\s+/', $string, 2, PREG_SPLIT_NO_EMPTY)) {
return self::ERROR;
}
foreach ($parts as $i => &$part) {
// Validate
if (!DateTimeObj::validate($part)) {
return self::ERROR;
}
$part = self::parseDate($part);
}
$string = $parts[0]['start'] . " to " . $parts[1]['end'];
}
// Parse the full date range and return an array
if (!$parts = preg_split('/\s+to\s+/i', $string, 2, PREG_SPLIT_NO_EMPTY)) {
return self::ERROR;
}
$parts = array_map(array('self', 'cleanFilterString'), $parts);
list($start, $end) = $parts;
// Validate
if (!DateTimeObj::validate($start) || !DateTimeObj::validate($end)) {
return self::ERROR;
}
$string = array('start' => $start, 'end' => $end);
return self::RANGE;
}
/**
* @deprecated @since Symphony 3.0.0
* Use EntryQueryAuthorAdapter class instead
*
* @param string $string
* @return string
*/
public static function cleanFilterString($string)
{
$string = trim($string, ' -/');
return urldecode($string);
}
/**
* @deprecated @since Symphony 3.0.0
*
* @param array $data
* @param string $joins
* @param string $where
* @param boolean $andOperation
* @return void
*/
public function buildRangeFilterSQL($data, &$joins, &$where, $andOperation = false)
{
$field_id = $this->get('id');
if (empty($data)) {
return;
}
if ($andOperation) {
foreach ($data as $date) {
// Prevent the DateTimeObj creating a range that isn't supported by MySQL.
$start = ($date['start'] === self::$min_date) ? self::$min_date : DateTimeObj::getGMT('Y-m-d H:i:s', $date['start']);
$end = ($date['end'] === self::$max_date) ? self::$max_date : DateTimeObj::getGMT('Y-m-d H:i:s', $date['end']);
$joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id".$this->key."` ON `e`.`id` = `t$field_id".$this->key."`.entry_id ";
$where .= " AND (`t$field_id".$this->key."`.date >= '" . $start . "' AND `t$field_id".$this->key."`.date <= '" . $end . "') ";
$this->key++;
}
} else {
$tmp = array();
foreach ($data as $date) {
// Prevent the DateTimeObj creating a range that isn't supported by MySQL.
$start = ($date['start'] === self::$min_date) ? self::$min_date : DateTimeObj::getGMT('Y-m-d H:i:s', $date['start']);
$end = ($date['end'] === self::$max_date) ? self::$max_date : DateTimeObj::getGMT('Y-m-d H:i:s', $date['end']);
$tmp[] = "`t$field_id".$this->key."`.date >= '" . $start . "' AND `t$field_id".$this->key."`.date <= '" . $end . "' ";
}
$joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t$field_id".$this->key."` ON `e`.`id` = `t$field_id".$this->key."`.entry_id ";
$where .= " AND (".implode(' OR ', $tmp).") ";
$this->key++;
}
}
/**
* Format the $data parameter according to this field's settings.
*
* @since Symphony 2.6.0
* @param array $date
* The date to format
* @return string
*/
public function formatDate($date)
{
// Get format
$format = 'date_format';
if ($this->get('time') === 'yes') {
$format = 'datetime_format';
}
return DateTimeObj::format($date, DateTimeObj::getSetting($format));
}
/*-------------------------------------------------------------------------
Settings:
-------------------------------------------------------------------------*/
public function findDefaults(array &$settings)
{
if (!isset($settings['pre_populate'])) {
$settings['pre_populate'] = $this->get('pre_populate');
}
}
public function displaySettingsPanel(XMLElement &$wrapper, $errors = null)
{
parent::displaySettingsPanel($wrapper, $errors);
// Default date
$label = Widget::Label(__('Default date'));
$help = new XMLElement('i', __('optional, accepts absolute or relative dates'));
$input = Widget::Input('fields['.$this->get('sortorder').'][pre_populate]', $this->get('pre_populate') ? $this->get('pre_populate') : '', 'input');
$label->appendChild($help);
$label->appendChild($input);
$wrapper->appendChild($label);
// Display settings
$div = new XMLElement('div', null, array('class' => 'two columns'));
$this->createCheckboxSetting($div, 'time', __('Display time'));
$this->createCheckboxSetting($div, 'calendar', __('Show calendar'));
$wrapper->appendChild($div);
// Requirements and table display
$this->appendStatusFooter($wrapper);
}
public function commit()
{
if (!parent::commit()) {
return false;
}
$id = $this->get('id');
if ($id === false) {
return false;
}
$fields = array();
$fields['pre_populate'] = ($this->get('pre_populate') ? $this->get('pre_populate') : '');
$fields['time'] = ($this->get('time') ? $this->get('time') : 'no');
$fields['calendar'] = ($this->get('calendar') ? $this->get('calendar') : 'no');
return FieldManager::saveSettings($id, $fields);
}
/*-------------------------------------------------------------------------
Publish:
-------------------------------------------------------------------------*/
public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null)
{
$name = $this->get('element_name');
$value = null;
// New entry
if (empty($data) && is_null($flagWithError) && !is_null($this->get('pre_populate'))) {
$prepopulate = $this->get('pre_populate');
$date = self::parseDate($prepopulate);
$date = $date['start'];
$value = $this->formatDate($date);
// Error entry, display original data
} elseif (!is_null($flagWithError)) {
$value = $_POST['fields'][$name];
// Empty entry
} elseif (isset($data['value'])) {
$value = $this->formatDate($data['value']);
}
$label = Widget::Label($this->get('label'));
if ($this->get('required') !== 'yes') {
$label->appendChild(new XMLElement('i', __('Optional')));
}
// Input
$label->appendChild(Widget::Input("fields{$fieldnamePrefix}[{$name}]", $value));
$label->setAttribute('class', 'date');
// Calendar
if ($this->get('calendar') === 'yes') {
$wrapper->setAttribute('data-interactive', 'data-interactive');
$ul = new XMLElement('ul');
$ul->setAttribute('class', 'suggestions');
$ul->setAttribute('data-field-id', $this->get('id'));
$ul->setAttribute('data-search-types', 'date');
$calendar = new XMLElement('li');
$calendar->appendChild(Widget::Calendar(($this->get('time') === 'yes')));
$ul->appendChild($calendar);
$label->appendChild($ul);
}
// Wrap label in error
if (!is_null($flagWithError)) {
$label = Widget::Error($label, $flagWithError);
}
$wrapper->appendChild($label);
}
public function checkPostFieldData($data, &$message, $entry_id = null)
{
$message = null;
// If this field is required
if ($this->get('required') === 'yes' && strlen(trim($data)) == 0) {
$message = __('ā%sā is a required field.', array($this->get('label')));
return self::__MISSING_FIELDS__;
} elseif (empty($data)) {
return self::__OK__;
}
// Handle invalid dates
if (!DateTimeObj::validate($data)) {
$message = __('The date specified in ā%sā is invalid.', array($this->get('label')));
return self::__INVALID_FIELDS__;
}
return self::__OK__;
}
public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null)
{
$status = self::__OK__;
$timestamp = null;
// Prepopulate date
if (is_null($data) || $data == '') {
if ($this->get('pre_populate') !='') {
$date = self::parseDate($this->get('pre_populate'));
$date = $date['start'];
$timestamp = $this->formatDate($date);
}
// Convert given date to timestamp
} elseif ($status == self::__OK__ && DateTimeObj::validate($data)) {
$timestamp = DateTimeObj::get('U', $data);
}
// Valid date
if (!is_null($timestamp)) {
return [
'value' => DateTimeObj::get('c', $timestamp),
];
// Invalid date
} else {
return array(
'value' => null
);
}
}
/*-------------------------------------------------------------------------
Output:
-------------------------------------------------------------------------*/
public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null)
{
if (isset($data['value'])) {
// Get date
if (is_array($data['value'])) {
$date = current($data['value']);
} else {
$date = $data['value'];
}
$wrapper->appendChild(General::createXMLDateObject($date, $this->get('element_name')));
}
}
public function prepareTextValue($data, $entry_id = null)
{
$value = '';
if (isset($data['value'])) {
$value = $this->formatDate($data['value']);
}
return $value;
}
public function getParameterPoolValue(array $data, $entry_id = null)
{
return DateTimeObj::get('Y-m-d H:i:s', $data['value']);
}
/*-------------------------------------------------------------------------
Import:
-------------------------------------------------------------------------*/
public function getImportModes()
{
return array(
'getValue' => ImportableField::STRING_VALUE,
'getPostdata' => ImportableField::ARRAY_VALUE
);
}
public function prepareImportValue($data, $mode, $entry_id = null)
{
$value = $status = $message = null;
$modes = (object)$this->getImportModes();
// Prepopulate date:
if ($data === null || $data === '') {
if (!is_null($this->get('pre_populate'))) {
$timestamp = self::parseDate($this->get('pre_populate'));
$timestamp = $timestamp['start'];
}
// DateTime to timestamp:
} elseif ($data instanceof DateTime) {
$timestamp = $data->getTimestamp();
// Convert given date to timestamp:
} elseif (DateTimeObj::validate($data)) {
$timestamp = DateTimeObj::get('U', $data);
}
// Valid date found:
if (isset($timestamp)) {
$value = DateTimeObj::get('c', $timestamp);
}
if ($mode === $modes->getValue) {
return $value;
} elseif ($mode === $modes->getPostdata) {
return $this->processRawFieldData($data, $status, $message, true, $entry_id);
}
return null;
}
/*-------------------------------------------------------------------------
Export:
-------------------------------------------------------------------------*/
/**
* Return a list of supported export modes for use with `prepareExportValue`.
*
* @return array
*/
public function getExportModes()
{
return array(
'getValue' => ExportableField::VALUE,
'getObject' => ExportableField::OBJECT,
'getPostdata' => ExportableField::POSTDATA
);
}
/**
* Give the field some data and ask it to return a value using one of many
* possible modes.
*
* @param mixed $data
* @param integer $mode
* @param integer $entry_id
* @return DateTime|null
*/
public function prepareExportValue($data, $mode, $entry_id = null)
{
$modes = (object)$this->getExportModes();
if ($mode === $modes->getValue) {
return $this->formatDate(
isset($data['value']) ? $data['value'] : null
);
}
if ($mode === $modes->getObject) {
$timezone = Symphony::Configuration()->get('timezone', 'region');
$date = new DateTime(
isset($data['value']) ? $data['value'] : 'now'
);
$date->setTimezone(new DateTimeZone($timezone));
return $date;
} elseif ($mode === $modes->getPostdata) {
return isset($data['value'])
? $data['value']
: null;
}
return null;
}
/*-------------------------------------------------------------------------
Filtering:
-------------------------------------------------------------------------*/
/**
* @deprecated @since Symphony 3.0.0
* @see Field::buildDSRetrievalSQL()
*/
public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildDSRetrievalSQL()',
'EntryQueryFieldAdapter::filter()'
);
}
if (self::isFilterRegex($data[0])) {
$this->buildRegexSQL($data[0], array('value'), $joins, $where);
} elseif (self::isFilterSQL($data[0])) {
$this->buildFilterSQL($data[0], array('value'), $joins, $where);
} else {
$parsed = array();
// For the filter provided, loop over each piece
foreach ($data as $string) {
$type = self::parseFilter($string);
if ($type == self::ERROR) {
return false;
}
if (!is_array($parsed[$type])) {
$parsed[$type] = array();
}
$parsed[$type][] = $string;
}
foreach ($parsed as $value) {
$this->buildRangeFilterSQL($value, $joins, $where, $andOperation);
}
}
return true;
}
/*-------------------------------------------------------------------------
Sorting:
-------------------------------------------------------------------------*/
/**
* @deprecated @since Symphony 3.0.0
* @see Field::buildSortingSQL()
*/
public function buildSortingSQL(&$joins, &$where, &$sort, $order = 'ASC')
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildSortingSQL()',
'EntryQueryFieldAdapter::sort()'
);
}
if ($this->isRandomOrder($order)) {
$sort = 'ORDER BY RAND()';
} else {
$sort = sprintf(
'ORDER BY (
SELECT %s
FROM tbl_entries_data_%d AS `ed`
WHERE entry_id = e.id
) %s, `e`.`id` %s',
'`ed`.date',
$this->get('id'),
$order,
$order
);
}
}
/**
* @deprecated @since Symphony 3.0.0
* @see Field::buildSortingSelectSQL()
*/
public function buildSortingSelectSQL($sort, $order = 'ASC')
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildSortingSelectSQL()',
'EntryQueryFieldAdapter::sort()'
);
}
return null;
}
/*-------------------------------------------------------------------------
Grouping:
-------------------------------------------------------------------------*/
public function groupRecords($records)
{
if (!is_array($records) || empty($records)) {
return;
}
$groups = array('year' => array());
foreach ($records as $r) {
$data = $r->getData($this->get('id'));
$timestamp = DateTimeObj::get('U', $data['value']);
$info = getdate($timestamp);
$year = $info['year'];
$month = ($info['mon'] < 10 ? '0' . $info['mon'] : $info['mon']);
if (!isset($groups['year'][$year])) {
$groups['year'][$year] = array(
'attr' => array('value' => $year),
'records' => array(),
'groups' => array()
);
}
if (!isset($groups['year'][$year]['groups']['month'])) {
$groups['year'][$year]['groups']['month'] = array();
}
if (!isset($groups['year'][$year]['groups']['month'][$month])) {
$groups['year'][$year]['groups']['month'][$month] = array(
'attr' => array('value' => $month),
'records' => array(),
'groups' => array()
);
}
$groups['year'][$year]['groups']['month'][$month]['records'][] = $r;
}
return $groups;
}
}