diff --git a/ORM/FieldType/DBDate.php b/ORM/FieldType/DBDate.php index b7daea3c2cb..d10c651ce53 100644 --- a/ORM/FieldType/DBDate.php +++ b/ORM/FieldType/DBDate.php @@ -36,10 +36,10 @@ class DBDate extends DBField { /** * @config * @see DBDateTime::nice_format - * @see Time::nice_format + * @see DBTime::nice_format */ private static $nice_format = 'd/m/Y'; - + public function setValue($value, $record = null, $markChanged = true) { if($value === false || $value === null || (is_string($value) && !strlen($value))) { // don't try to evaluate empty values with strtotime() below, as it returns "1970-01-01" when it should be @@ -80,44 +80,56 @@ public function setValue($value, $record = null, $markChanged = true) { /** * Returns the date in the format specified by the config value nice_format, or dd/mm/yy by default - */ + * + * @return string + */ public function Nice() { - if($this->value) return $this->Format($this->config()->nice_format); + return $this->Format($this->config()->nice_format); } /** * Returns the date in US format: “01/18/2006” + * + * @return string */ public function NiceUS() { - if($this->value) return $this->Format('m/d/Y'); + return $this->Format('m/d/Y'); } /** * Returns the year from the given date + * + * @return string */ public function Year() { - if($this->value) return $this->Format('Y'); + return $this->Format('Y'); } /** * Returns the Full day, of the given date. + * + * @return string */ public function Day(){ - if($this->value) return $this->Format('l'); + return $this->Format('l'); } /** * Returns a full textual representation of a month, such as January. + * + * @return string */ public function Month() { - if($this->value) return $this->Format('F'); + return $this->Format('F'); } /** * Returns the short version of the month such as Jan + * + * @return string */ public function ShortMonth() { - if($this->value) return $this->Format('M'); + return $this->Format('M'); } /** @@ -126,25 +138,27 @@ public function ShortMonth() { * @return string */ public function DayOfMonth($includeOrdinal = false) { - if($this->value) { $format = 'j'; if ($includeOrdinal) $format .= 'S'; return $this->Format($format); } - } /** * Returns the date in the format 24 December 2006 + * + * @return string */ public function Long() { - if($this->value) return $this->Format('j F Y'); + return $this->Format('j F Y'); } /** * Returns the date in the format 24 Dec 2006 + * + * @return string */ public function Full() { - if($this->value) return $this->Format('j M Y'); + return $this->Format('j M Y'); } /** @@ -158,6 +172,7 @@ public function Format($format) { $date = new DateTime($this->value); return $date->format($format); } + return null; } /** @@ -173,6 +188,7 @@ public function FormatI18N($formattingString) { if($this->value) { return strftime($formattingString, strtotime($this->value)); } + return null; } /** @@ -217,12 +233,28 @@ public function RangeString($otherDateObj, $includeOrdinals = false) { else return "$d1 - $d2 $m1 $y1"; } + /** + * Return string in RFC822 format + * + * @return string + */ public function Rfc822() { - if($this->value) return date('r', strtotime($this->value)); + if($this->value) { + return date('r', strtotime($this->value)); + } + return null; } + /** + * Return date in RFC2822 format + * + * @return string + */ public function Rfc2822() { - if($this->value) return date('Y-m-d H:i:s', strtotime($this->value)); + if($this->value) { + return date('Y-m-d H:i:s', strtotime($this->value)); + } + return null; } public function Rfc3339() { @@ -249,7 +281,9 @@ public function Rfc3339() { * @return String */ public function Ago($includeSeconds = true, $significance = 2) { - if($this->value) { + if(!$this->value) { + return null; + } $time = DBDatetime::now()->Format('U'); if(strtotime($this->value) == $time || $time > strtotime($this->value)) { return _t( @@ -267,7 +301,6 @@ public function Ago($includeSeconds = true, $significance = 2) { ); } } - } /** * @param boolean $includeSeconds Show seconds, or just round to "less than a minute". @@ -304,7 +337,9 @@ public function TimeDiff($includeSeconds = true, $significance = 2) { * @return string The resulting formatted period */ public function TimeDiffIn($format) { - if(!$this->value) return false; + if(!$this->value) { + return null; + } $time = DBDatetime::now()->Format('U'); $ago = abs($time - strtotime($this->value)); @@ -333,6 +368,9 @@ public function TimeDiffIn($format) { case "years": $span = round($ago/86400/365); return ($span != 1) ? "{$span} "._t("Date.YEARS", "years") : "{$span} "._t("Date.YEAR", "year"); + + default: + throw new \InvalidArgumentException("Invalid format $format"); } } diff --git a/ORM/FieldType/DBDatetime.php b/ORM/FieldType/DBDatetime.php index bab437f049e..e43e9e692ae 100644 --- a/ORM/FieldType/DBDatetime.php +++ b/ORM/FieldType/DBDatetime.php @@ -5,6 +5,7 @@ use Convert; use Exception; use DatetimeField; +use InvalidArgumentException; use Zend_Date; use TemplateGlobalProvider; use DateTime; @@ -75,42 +76,47 @@ public function setValue($value, $record = null, $markChanged = true) { /** * Returns the date and time in the format specified by the config value nice_format, or 'd/m/Y g:ia' * by default (e.g. '31/01/2014 2:23pm'). + * * @return string Formatted date and time. */ public function Nice() { - if($this->value) return $this->Format($this->config()->nice_format); + return $this->Format($this->config()->nice_format); } /** * Returns the date and time (in 24-hour format) using the format string 'd/m/Y H:i' e.g. '28/02/2014 13:32'. + * * @return string Formatted date and time. */ public function Nice24() { - if($this->value) return $this->Format('d/m/Y H:i'); + return $this->Format('d/m/Y H:i'); } /** * Returns the date using the format string 'd/m/Y' e.g. '28/02/2014'. + * * @return string Formatted date. */ public function Date() { - if($this->value) return $this->Format('d/m/Y'); + return $this->Format('d/m/Y'); } /** * Returns the time in 12-hour format using the format string 'g:ia' e.g. '1:32pm'. + * * @return string Formatted time. */ public function Time() { - if($this->value) return $this->Format('g:ia'); + return $this->Format('g:ia'); } /** * Returns the time in 24-hour format using the format string 'H:i' e.g. '13:32'. + * * @return string Formatted time. */ public function Time24() { - if($this->value) return $this->Format('H:i'); + return $this->Format('H:i'); } /** @@ -149,7 +155,7 @@ public function requireField() { * @return string Formatted date and time. */ public function URLDatetime() { - if($this->value) return $this->Format('Y-m-d%20H:i:s'); + return $this->Format('Y-m-d%20H:i:s'); } public function scaffoldFormField($title = null, $params = null) { @@ -187,7 +193,7 @@ public static function now() { if(self::$mock_now) { return self::$mock_now; } else { - return DBField::create_field('SilverStripe\ORM\FieldType\DBDatetime', date('Y-m-d H:i:s')); + return DBField::create_field('Datetime', date('Y-m-d H:i:s')); } } @@ -203,9 +209,9 @@ public static function set_mock_now($datetime) { if($datetime instanceof DBDatetime) { self::$mock_now = $datetime; } elseif(is_string($datetime)) { - self::$mock_now = DBField::create_field('SilverStripe\ORM\FieldType\DBDatetime', $datetime); + self::$mock_now = DBField::create_field('Datetime', $datetime); } else { - throw new Exception('DBDatetime::set_mock_now(): Wrong format: ' . $datetime); + throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $datetime); } } @@ -219,7 +225,7 @@ public static function clear_mock_now() { public static function get_template_global_variables() { return array( - 'Now' => array('method' => 'now', 'casting' => 'SilverStripe\ORM\FieldType\DBDatetime'), + 'Now' => array('method' => 'now', 'casting' => 'Datetime'), ); } } diff --git a/ORM/FieldType/DBEnum.php b/ORM/FieldType/DBEnum.php index 1f55c1de797..efa7d63eec2 100644 --- a/ORM/FieldType/DBEnum.php +++ b/ORM/FieldType/DBEnum.php @@ -51,10 +51,8 @@ class DBEnum extends DBString { * * * @param string $name - * @param string|array $enum A string containing a comma separated list of options or an - * array of Vals. - * @param string $default The default option, which is either NULL or one of the - * items in the enumeration. + * @param string|array $enum A string containing a comma separated list of options or an array of Vals. + * @param string $default The default option, which is either NULL or one of the items in the enumeration. */ public function __construct($name = null, $enum = NULL, $default = NULL) { if($enum) { @@ -107,10 +105,14 @@ public function requireField() { /** * Return a dropdown field suitable for editing this field. * + * @param string $title + * @param string $name + * @param bool $hasEmpty + * @param string $value + * @param string $emptyString * @return DropdownField */ - public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $form = null, - $emptyString = null) { + public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $emptyString = null) { if(!$title) { $title = $this->getName(); @@ -119,7 +121,7 @@ public function formField($title = null, $name = null, $hasEmpty = false, $value $name = $this->getName(); } - $field = new DropdownField($name, $title, $this->enumValues(false), $value, $form); + $field = new DropdownField($name, $title, $this->enumValues(false), $value); if($hasEmpty) { $field->setEmptyString($emptyString); } @@ -127,10 +129,7 @@ public function formField($title = null, $name = null, $hasEmpty = false, $value return $field; } - /** - * @return DropdownField - */ - public function scaffoldFormField($title = null, $params = null) { + public function scaffoldFormField($title = null) { return $this->formField($title); } @@ -141,7 +140,7 @@ public function scaffoldFormField($title = null, $params = null) { */ public function scaffoldSearchField($title = null) { $anyText = _t('Enum.ANY', 'Any'); - return $this->formField($title, null, true, $anyText, null, "($anyText)"); + return $this->formField($title, null, true, $anyText, "($anyText)"); } /** diff --git a/ORM/FieldType/DBField.php b/ORM/FieldType/DBField.php index fe400324919..2a11b590771 100644 --- a/ORM/FieldType/DBField.php +++ b/ORM/FieldType/DBField.php @@ -4,8 +4,8 @@ use FormField; use SearchFilter; -use SilverStripe\ORM\Connect\SS_Query; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\Queries\SQLSelect; use ViewableData; use Convert; use Object; @@ -256,7 +256,7 @@ public function writeToManipulation(&$manipulation) { * gets you the default representations * of all columns. * - * @param SS_Query $query + * @param SQLSelect $query */ public function addToQuery(&$query) { @@ -288,7 +288,8 @@ public function getTable() { * @return string */ public function forTemplate() { - return Convert::raw2xml($this->getValue()); + // Default to XML encoding + return $this->XML(); } /** @@ -369,7 +370,7 @@ public function HTML(){ * * @return string */ - public function XML(){ + public function XML() { return Convert::raw2xml($this->RAW()); } @@ -379,7 +380,7 @@ public function XML(){ * @return string */ public function CDATA() { - return $this->forTemplate(); + return $this->XML(); } /** @@ -402,7 +403,7 @@ public function saveInto($dataObject) { if(empty($fieldName)) { throw new \BadMethodCallException("DBField::saveInto() Called on a nameless '" . get_class($this) . "' object"); } - $dataObject->$fieldName = $this->value; + $dataObject->$fieldName = $this->value; } /** @@ -440,7 +441,6 @@ public function scaffoldSearchField($title = null) { * search filters (note: parameter hack now in place to pass in the required full path - using $this->name * won't work) * - * @param string|bool $name * @param string $name Override name of this field * @return SearchFilter */ diff --git a/ORM/FieldType/DBHTMLText.php b/ORM/FieldType/DBHTMLText.php index 43ad94eb785..df5983edb52 100644 --- a/ORM/FieldType/DBHTMLText.php +++ b/ORM/FieldType/DBHTMLText.php @@ -4,11 +4,9 @@ use Injector; use HTTP; -use ShortcodeParser; -use DOMDocument; use HTMLEditorField; +use ShortcodeParser; use TextField; -use Exception; /** * Represents a large text field that contains HTML content. @@ -32,20 +30,10 @@ class DBHTMLText extends DBText { private static $casting = array( "AbsoluteLinks" => "HTMLFragment", - // DBText summary methods - override to HTMLFragment - "BigSummary" => "HTMLFragment", - "ContextSummary" => "HTMLFragment", // Same as DBText - "FirstParagraph" => "HTMLFragment", - "FirstSentence" => "HTMLFragment", - "LimitSentences" => "HTMLFragment", - "Summary" => "HTMLFragment", - // DBString conversion / summary methods - override to HTMLFragment - "LimitCharacters" => "HTMLFragment", - "LimitCharactersToClosestWord" => "HTMLFragment", - "LimitWordCount" => "HTMLFragment", + // DBString conversion / summary methods + // Not overridden, but returns HTML instead of plain text. "LowerCase" => "HTMLFragment", "UpperCase" => "HTMLFragment", - "NoHTML" => "Text", // Actually stays same as DBString cast ); /** @@ -134,141 +122,6 @@ public function setOptions(array $options = array()) { return parent::setOptions($options); } - public function LimitSentences($maxSentences = 2) - { - // @todo - return parent::LimitSentences($maxSentences); - } - - public function LimitWordCount($numWords = 26, $add = '...') - { - // @todo - return parent::LimitWordCount($numWords, $add); - } - - public function LimitCharacters($limit = 20, $add = '...') - { - // @todo - return parent::LimitCharacters($limit, $add); - } - - public function LimitCharactersToClosestWord($limit = 20, $add = '...') - { - // @todo - return parent::LimitCharactersToClosestWord($limit, $add); - } - - public function BigSummary($maxWords = 50) - { - // @todo - return parent::BigSummary($maxWords); // TODO: Change the autogenerated stub - } - - - /** - * Create a summary of the content. This will be some section of the first paragraph, limited by - * $maxWords. All internal tags are stripped out - the return value is a string - * - * This is sort of the HTML aware equivilent to Text#Summary, although the logic for summarising is not exactly - * the same - * - * @param int $maxWords Maximum number of words to return - may return less, but never more. Pass -1 for no limit - * @param int $flex Number of words to search through when looking for a nice cut point - * @param string $add What to add to the end of the summary if we cut at a less-than-ideal cut point - * @return string A nice(ish) summary with no html tags (but possibly still some html entities) - * - * @see framework/core/model/fieldtypes/Text#Summary($maxWords) - */ - public function Summary($maxWords = 50, $flex = 15, $add = '...') { - $str = false; - - /* First we need the text of the first paragraph, without tags. Try using SimpleXML first */ - if (class_exists('SimpleXMLElement')) { - $doc = new DOMDocument(); - - // Catch warnings thrown by loadHTML and turn them into a failure boolean rather than a SilverStripe error - set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL); - // Nonbreaking spaces get converted into weird characters, so strip them - $value = str_replace(' ', ' ', $this->RAW()); - try { - $res = $doc->loadHTML('' . $value); - } - catch (Exception $e) { $res = false; } - restore_error_handler(); - - if ($res) { - $xml = simplexml_import_dom($doc); - $res = $xml->xpath('//p'); - if (!empty($res)) $str = strip_tags($res[0]->asXML()); - } - } - - /* If that failed, most likely the passed HTML is broken. use a simple regex + a custom more brutal strip_tags. - * We don't use strip_tags because that does very badly on broken HTML */ - if (!$str) { - /* See if we can pull a paragraph out*/ - - // Strip out any images in case there's one at the beginning. Not doing this will return a blank paragraph - $str = preg_replace('{^\s*(<.+?>)*]*>}', '', $this->value); - if (preg_match('{]*)?>(.*[A-Za-z]+.*)

}', $str, $matches)) $str = $matches[2]; - - /* If _that_ failed, just use the whole text */ - if (!$str) $str = $this->value; - - /* Now pull out all the html-alike stuff */ - /* Take out anything that is obviously a tag */ - $str = preg_replace('{]*>}', '', $str); - /* Strip out any left over looking bits. Textual < or > should already be encoded to < or > */ - $str = preg_replace('{}', '', $str); - } - - /* Now split into words. If we are under the maxWords limit, just return the whole string (re-implode for - * whitespace normalization) */ - $words = preg_split('/\s+/', $str); - if ($maxWords == -1 || count($words) <= $maxWords) return implode(' ', $words); - - /* Otherwise work backwards for a looking for a sentence ending (we try to avoid abbreviations, but aren't - * very good at it) */ - for ($i = $maxWords; $i >= $maxWords - $flex && $i >= 0; $i--) { - if (preg_match('/\.$/', $words[$i]) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $words[$i])) { - return implode(' ', array_slice($words, 0, $i+1)); - } - } - - // If we didn't find a sentence ending quickly enough, just cut at the maxWords point and add '...' to the end - return implode(' ', array_slice($words, 0, $maxWords)) . $add; - } - - public function FirstParagraph() { - // @todo implement - return parent::FirstParagraph(); - } - - /** - * Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or - * there isn't one), it returns the same as Summary() - * - * This is the HTML aware equivilent to Text#FirstSentence - * - * @see framework/core/model/fieldtypes/Text#FirstSentence() - */ - public function FirstSentence() { - /* Use summary's html processing logic to get the first paragraph */ - $paragraph = $this->Summary(-1); - - /* Then look for the first sentence ending. We could probably use a nice regex, but for now this will do */ - $words = preg_split('/\s+/', $paragraph); - foreach ($words as $i => $word) { - if (preg_match('/(!|\?|\.)$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) { - return implode(' ', array_slice($words, 0, $i+1)); - } - } - - /* If we didn't find a sentence ending, use the summary. We re-call rather than using paragraph so that - * Summary will limit the result this time */ - return $this->Summary(); - } - public function RAW() { if ($this->processShortcodes) { return ShortcodeParser::get_active()->parse($this->value); @@ -287,6 +140,7 @@ public function AbsoluteLinks() { } public function forTemplate() { + // Suppress XML encoding for DBHtmlText return $this->RAW(); } @@ -364,23 +218,34 @@ public function exists() { return true; } - public function scaffoldFormField($title = null, $params = null) { + public function scaffoldFormField($title = null) { return new HTMLEditorField($this->name, $title); } - public function scaffoldSearchField($title = null, $params = null) { + public function scaffoldSearchField($title = null) { return new TextField($this->name, $title); } /** + * Get plain-text version + * * @return string */ - public function NoHTML() - { + public function Plain() { // Preserve line breaks $text = preg_replace('/\/i', "\n", $this->RAW()); - // Convert back to plain text - return \Convert::xml2raw(strip_tags($text)); + + // Convert paragraph breaks to multi-lines + $text = preg_replace('/\<\/p\>/i', "\n\n", $text); + + // Strip out HTML tags + $text = strip_tags($text); + + // Implode >3 consecutive linebreaks into 2 + $text = preg_replace('~(\R){2,}~', "\n\n", $text); + + // Decode HTML entities back to plain text + return trim(\Convert::xml2raw($text)); } } diff --git a/ORM/FieldType/DBHTMLVarchar.php b/ORM/FieldType/DBHTMLVarchar.php index c8affc3c196..a8b0ebd7563 100644 --- a/ORM/FieldType/DBHTMLVarchar.php +++ b/ORM/FieldType/DBHTMLVarchar.php @@ -17,6 +17,13 @@ class DBHTMLVarchar extends DBVarchar { private static $escape_type = 'xml'; + private static $casting = array( + // DBString conversion / summary methods + // Not overridden, but returns HTML instead of plain text. + "LowerCase" => "HTMLFragment", + "UpperCase" => "HTMLFragment", + ); + /** * Enable shortcode parsing on this field * @@ -69,6 +76,7 @@ public function setOptions(array $options = array()) { } public function forTemplate() { + // Suppress XML encoding for DBHtmlText return $this->RAW(); } @@ -88,15 +96,53 @@ public function RAW() { public function CDATA() { return sprintf( '', - str_replace(']]>', ']]]]>', $this->forTemplate()) + str_replace(']]>', ']]]]>', $this->RAW()) ); } + /** + * Get plain-text version. + * + * Note: unlike DBHTMLText, this doesn't respect line breaks / paragraphs + * + * @return string + */ + public function Plain() { + // Strip out HTML + $text = strip_tags($this->RAW()); + + // Convert back to plain text + return trim(\Convert::xml2raw($text)); + } + + /** + * Returns true if the field has meaningful content. + * Excludes null content like

,

,etc + * + * @return boolean + */ public function exists() { - return parent::exists() && $this->RAW() != '

'; + // If it's blank, it's blank + if(!parent::exists()) { + return false; + } + + // If it's got a content tag + if(preg_match('/<(img|embed|object|iframe|meta|source|link)[^>]*>/i', $this->RAW())) { + return true; + } + + // If it's just one or two tags on its own (and not the above) it's empty. + // This might be

or

or whatever. + if(preg_match('/^[\\s]*(<[^>]+>[\\s]*){1,2}$/', $this->RAW())) { + return false; + } + + // Otherwise its content is genuine content + return true; } - public function scaffoldFormField($title = null, $params = null) { + public function scaffoldFormField($title = null) { return new HTMLEditorField($this->name, $title, 1); } diff --git a/ORM/FieldType/DBMoney.php b/ORM/FieldType/DBMoney.php index 375f850925e..ae5c4a7633c 100644 --- a/ORM/FieldType/DBMoney.php +++ b/ORM/FieldType/DBMoney.php @@ -63,6 +63,7 @@ public function __construct($name = null) { } /** + * @param array $options * @return string */ public function Nice($options = array()) { diff --git a/ORM/FieldType/DBMultiEnum.php b/ORM/FieldType/DBMultiEnum.php index ea9efd42f1c..ccde67b830a 100644 --- a/ORM/FieldType/DBMultiEnum.php +++ b/ORM/FieldType/DBMultiEnum.php @@ -54,14 +54,20 @@ public function requireField(){ /** * Return a {@link CheckboxSetField} suitable for editing this field + * + * @param string $title + * @param string $name + * @param bool $hasEmpty + * @param string $value + * @param string $emptyString + * @return CheckboxSetField */ - public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $form = null, - $emptyString = null) { + public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $emptyString = null) { if(!$title) $title = $this->name; if(!$name) $name = $this->name; - $field = new CheckboxSetField($name, $title, $this->enumValues($hasEmpty), $value, $form); + $field = new CheckboxSetField($name, $title, $this->enumValues($hasEmpty), $value); return $field; } diff --git a/ORM/FieldType/DBPolymorphicForeignKey.php b/ORM/FieldType/DBPolymorphicForeignKey.php index af572a2f3f1..54611fbcb40 100644 --- a/ORM/FieldType/DBPolymorphicForeignKey.php +++ b/ORM/FieldType/DBPolymorphicForeignKey.php @@ -80,5 +80,6 @@ public function getValue() { if($id && $class && is_subclass_of($class, 'SilverStripe\ORM\DataObject')) { return DataObject::get_by_id($class, $id); } + return null; } } diff --git a/ORM/FieldType/DBPrimaryKey.php b/ORM/FieldType/DBPrimaryKey.php index 44ca89630f0..18513cbc1e6 100644 --- a/ORM/FieldType/DBPrimaryKey.php +++ b/ORM/FieldType/DBPrimaryKey.php @@ -5,7 +5,6 @@ use SilverStripe\ORM\DB; use SilverStripe\ORM\DataObject; - /** * A special type Int field used for primary keys. * diff --git a/ORM/FieldType/DBString.php b/ORM/FieldType/DBString.php index dda71107aac..8445f28d256 100644 --- a/ORM/FieldType/DBString.php +++ b/ORM/FieldType/DBString.php @@ -2,8 +2,6 @@ namespace SilverStripe\ORM\FieldType; -use Convert; - /** * An abstract base class for the string field types (i.e. Varchar and Text) * @@ -23,10 +21,10 @@ abstract class DBString extends DBField { private static $casting = array( "LimitCharacters" => "Text", "LimitCharactersToClosestWord" => "Text", - 'LimitWordCount' => 'Text', + "LimitWordCount" => "Text", "LowerCase" => "Text", "UpperCase" => "Text", - 'NoHTML' => 'Text', + "Plain" => "Text", ); /** @@ -129,10 +127,6 @@ public function exists() { || (!$this->getNullifyEmpty() && $value === ''); // Remove this stupid exemption in 4.0 } - /** - * (non-PHPdoc) - * @see core/model/fieldtypes/DBField#prepValueForDB($value) - */ public function prepValueForDB($value) { if(!$this->nullifyEmpty && $value === '') { return $value; @@ -158,17 +152,11 @@ public function forTemplate() { * @return string */ public function LimitCharacters($limit = 20, $add = '...') { - $value = trim($this->RAW()); - if($this->stat('escape_type') == 'xml') { - $value = strip_tags($value); - $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); - $value = (mb_strlen($value) > $limit) ? mb_substr($value, 0, $limit) . $add : $value; - // Avoid encoding all multibyte characters as HTML entities by using htmlspecialchars(). - $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - } else { - $value = (mb_strlen($value) > $limit) ? mb_substr($value, 0, $limit) . $add : $value; + $value = $this->Plain(); + if(mb_strlen($value) <= $limit) { + return $value; } - return $value; + return mb_substr($value, 0, $limit) . $add; } /** @@ -178,27 +166,26 @@ public function LimitCharacters($limit = 20, $add = '...') { * * @param int $limit Number of characters to limit by * @param string $add Ellipsis to add to the end of truncated string - * @return string + * @return string Plain text value with limited characters */ public function LimitCharactersToClosestWord($limit = 20, $add = '...') { - // Strip HTML tags if they exist in the field - $value = strip_tags($this->RAW()); + // Safely convert to plain text + $value = $this->Plain(); // Determine if value exceeds limit before limiting characters - $exceedsLimit = mb_strlen($value) > $limit; + if(mb_strlen($value) <= $limit) { + return $value; + } // Limit to character limit - $value = DBField::create_field(get_class($this), $value)->LimitCharacters($limit, ''); + $value = mb_substr($value, 0, $limit); // If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis - if($exceedsLimit) { - $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); - - $value = rtrim(mb_substr($value, 0, mb_strrpos($value, " ")), "/[\.,-\/#!$%\^&\*;:{}=\-_`~()]\s") . $add; - - $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - } - + $value = preg_replace( + '/[^\w_]+$/', + '', + mb_substr($value, 0, mb_strrpos($value, " ")) + ) . $add; return $value; } @@ -211,23 +198,21 @@ public function LimitCharactersToClosestWord($limit = 20, $add = '...') { * @return string */ public function LimitWordCount($numWords = 26, $add = '...') { - $value = trim(Convert::xml2raw($this->RAW())); - $ret = explode(' ', $value, $numWords + 1); - - if(count($ret) <= $numWords - 1) { - $ret = $value; - } else { - array_pop($ret); - $ret = implode(' ', $ret) . $add; + $value = $this->Plain(); + $words = explode(' ', $value); + if(count($words) <= $numWords) { + return $value; } - return $ret; + // Limit + $words = array_slice($words, 0, $numWords); + return implode(' ', $words) . $add; } /** * Converts the current value for this StringField to lowercase. * - * @return string + * @return string Text with lowercase (HTML for some subclasses) */ public function LowerCase() { return mb_strtolower($this->RAW()); @@ -236,7 +221,7 @@ public function LowerCase() { /** * Converts the current value for this StringField to uppercase. * - * @return string + * @return string Text with uppercase (HTML for some subclasses) */ public function UpperCase() { return mb_strtoupper($this->RAW()); @@ -247,7 +232,7 @@ public function UpperCase() { * * @return string Plain text */ - public function NoHTML() { - return $this->RAW(); + public function Plain() { + return trim($this->RAW()); } } diff --git a/ORM/FieldType/DBText.php b/ORM/FieldType/DBText.php index 5b50d89fb13..b8eaa67be90 100644 --- a/ORM/FieldType/DBText.php +++ b/ORM/FieldType/DBText.php @@ -32,7 +32,7 @@ class DBText extends DBString { private static $casting = array( "BigSummary" => "Text", - "ContextSummary" => "HTMLText", // Always returns HTML as it contains formatting and highlighting + "ContextSummary" => "HTMLFragment", // Always returns HTML as it contains formatting and highlighting "FirstParagraph" => "Text", "FirstSentence" => "Text", "LimitSentences" => "Text", @@ -72,9 +72,9 @@ public function requireField() { public function LimitSentences($maxSentences = 2) { if(!is_numeric($maxSentences)) { throw new InvalidArgumentException("Text::LimitSentence() expects one numeric argument"); - } + } - $value = $this->NoHTML(); + $value = $this->Plain(); if( !$value ) { return ""; } @@ -114,40 +114,32 @@ public function FirstSentence() { * Builds a basic summary, up to a maximum number of words * * @param int $maxWords - * @param int $maxParagraphs Optional paragraph limit + * @param string $add * @return string */ - public function Summary($maxWords = 50, $maxParagraphs = 1) { + public function Summary($maxWords = 50, $add = '...') { // Get plain-text version - $value = $this->NoHTML(); + $value = $this->Plain(); if(!$value) { return ''; - } - - // Set max paragraphs - if($maxParagraphs) { - // Split on >2 linebreaks - $paragraphs = preg_split('#\n{2,}#', $value); - if(count($paragraphs) > $maxParagraphs) { - $paragraphs = array_slice($paragraphs, 0, $maxParagraphs); } - $value = implode("\n\n", $paragraphs); - } - // Find sentences - $sentences = explode('.', $value); + // Split on sentences (don't remove period) + $sentences = array_filter(array_map(function($str) { + return trim($str); + }, preg_split('@(?<=\.)@', $value))); $wordCount = count(preg_split('#\s+#', $sentences[0])); // if the first sentence is too long, show only the first $maxWords words if($wordCount > $maxWords) { - return implode( ' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . '...'; + return implode( ' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . $add; } // add each sentence while there are enough words to do so $result = ''; do { // Add next sentence - $result .= ' ' . trim(array_shift( $sentences )).'.'; + $result .= ' ' . array_shift( $sentences ); // If more sentences to process, count number of words if($sentences) { @@ -158,31 +150,21 @@ public function Summary($maxWords = 50, $maxParagraphs = 1) { return trim($result); } - /** - * Performs the same function as the big summary, but doesn't trim new paragraphs off data. - * - * @param int $maxWords - * @return string - */ - public function BigSummary($maxWords = 50) { - return $this->Summary($maxWords, 0); - } - /** * Get first paragraph * * @return string */ public function FirstParagraph() { - $value = $this->NoHTML(); + $value = $this->Plain(); if(empty($value)) { return ''; - } + } // Split paragraphs and return first $paragraphs = preg_split('#\n{2,}#', $value); return reset($paragraphs); - } + } /** * Perform context searching to give some context to searches, optionally @@ -205,7 +187,7 @@ public function ContextSummary( } // Get raw text value, but XML encode it (as we'll be merging with HTML tags soon) - $text = nl2br(Convert::raw2xml($this->NoHTML())); + $text = nl2br(Convert::raw2xml($this->Plain())); $keywords = Convert::raw2xml($keywords); // Find the search string @@ -230,9 +212,10 @@ public function ContextSummary( if($stringPieces) { foreach($stringPieces as $stringPiece) { if(strlen($stringPiece) > 2) { - $summary = str_ireplace( - $stringPiece, - "$stringPiece", + // Maintain case of original string + $summary = preg_replace( + '/' . preg_quote($stringPiece, '/') . '/i', + '$0', $summary ); } @@ -245,7 +228,7 @@ public function ContextSummary( if($position > 0) { $summary = $prefix . $summary; } - if(strlen($this->value) > ($characters + $position)) { + if(strlen($text) > ($characters + $position)) { $summary = $summary . $suffix; } @@ -267,14 +250,10 @@ public function Parse($parser) { /** @var TextParser $obj */ $obj = \Injector::inst()->createWithArgs($parser, [$this->forTemplate()]); - return $obj->parse(); + return $obj->parse(); } - /** - * (non-PHPdoc) - * @see DBField::scaffoldFormField() - */ - public function scaffoldFormField($title = null, $params = null) { + public function scaffoldFormField($title = null) { if(!$this->nullifyEmpty) { // Allow the user to select if it's null instead of automatically assuming empty string is return new NullableField(new TextareaField($this->name, $title)); @@ -284,11 +263,7 @@ public function scaffoldFormField($title = null, $params = null) { } } - /** - * (non-PHPdoc) - * @see DBField::scaffoldSearchField() - */ - public function scaffoldSearchField($title = null, $params = null) { + public function scaffoldSearchField($title = null) { return new TextField($this->name, $title); } } diff --git a/ORM/FieldType/DBTime.php b/ORM/FieldType/DBTime.php index 106688c382b..420efb222f8 100644 --- a/ORM/FieldType/DBTime.php +++ b/ORM/FieldType/DBTime.php @@ -47,7 +47,7 @@ public function setValue($value, $record = null, $markChanged = true) { * @return string */ public function Nice() { - if($this->value) return $this->Format($this->config()->nice_format); + return $this->Format($this->config()->nice_format); } /** @@ -57,7 +57,7 @@ public function Nice() { * @return string Time in 24 hour format */ public function Nice24() { - if($this->value) return date('H:i', strtotime($this->value)); + return $this->Format('H:i'); } /** @@ -67,7 +67,10 @@ public function Nice24() { * @return string The date in the requested format */ public function Format($format) { - if($this->value) return date($format, strtotime($this->value)); + if($this->value) { + return date($format, strtotime($this->value)); + } + return null; } public function TwelveHour( $parts ) { diff --git a/ORM/FieldType/DBVarchar.php b/ORM/FieldType/DBVarchar.php index c0b8d683272..621a8748304 100644 --- a/ORM/FieldType/DBVarchar.php +++ b/ORM/FieldType/DBVarchar.php @@ -78,21 +78,29 @@ public function requireField() { /** * Return the first letter of the string followed by a . + * + * @return string */ public function Initial() { if($this->exists()) { $value = $this->RAW(); return $value[0] . '.'; } + return null; } /** * Ensure that the given value is an absolute URL. + * + * @return string */ public function URL() { $value = $this->RAW(); - if(preg_match('#^[a-zA-Z]+://#', $value)) return $value; - else return "http://" . $value; + if(preg_match('#^[a-zA-Z]+://#', $value)) { + return $value; + } else { + return "http://" . $value; + } } /** @@ -103,11 +111,7 @@ public function RTF() { return str_replace("\n", '\par ', $this->RAW()); } - /** - * (non-PHPdoc) - * @see DBField::scaffoldFormField() - */ - public function scaffoldFormField($title = null, $params = null) { + public function scaffoldFormField($title = null) { if(!$this->nullifyEmpty) { // Allow the user to select if it's null instead of automatically assuming empty string is return new NullableField(new TextField($this->name, $title)); diff --git a/ORM/FieldType/DBYear.php b/ORM/FieldType/DBYear.php index 9e4785f5363..4c18e5114b8 100644 --- a/ORM/FieldType/DBYear.php +++ b/ORM/FieldType/DBYear.php @@ -40,9 +40,13 @@ public function scaffoldFormField($title = null, $params = null) { * @param int|bool $end end date to count down to * @return array */ - private function getDefaultOptions($start=false, $end=false) { - if (!$start) $start = (int)date('Y'); - if (!$end) $end = 1900; + private function getDefaultOptions($start = null, $end = null) { + if (!$start) { + $start = (int)date('Y'); + } + if (!$end) { + $end = 1900; + } $years = array(); for($i=$start;$i>=$end;$i--) { $years[$i] = $i; diff --git a/api/RSSFeed.php b/api/RSSFeed.php index 01f838fd059..f3023eb4a48 100644 --- a/api/RSSFeed.php +++ b/api/RSSFeed.php @@ -311,7 +311,14 @@ public function Title() { * @return DBField Returns the description of the entry. */ public function Description() { - return $this->rssField($this->descriptionField); + $description = $this->rssField($this->descriptionField); + + // HTML fields need links re-written + if($description instanceof DBHTMLText) { + return $description->obj('AbsoluteLinks'); + } + + return $description; } /** diff --git a/core/Convert.php b/core/Convert.php index ebff466e244..4091e947837 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -191,6 +191,8 @@ public static function symbol2sql($identifier, $separator = '.') { * Convert XML to raw text. * @uses html2raw() * @todo Currently &#xxx; entries are stripped; they should be converted + * @param mixed $val + * @return array|string */ public static function xml2raw($val) { if(is_array($val)) { @@ -234,6 +236,7 @@ public static function json2array($val) { * false by default. * @param boolean $disableExternals Disables the loading of external entities. false by default. * @return array + * @throws Exception */ public static function xml2array($val, $disableDoctypes = false, $disableExternals = false) { // Check doctype @@ -242,6 +245,7 @@ public static function xml2array($val, $disableDoctypes = false, $disableExterna } // Disable external entity loading + $oldVal = null; if($disableExternals) $oldVal = libxml_disable_entity_loader($disableExternals); try { $xml = new SimpleXMLElement($val); @@ -263,7 +267,7 @@ public static function xml2array($val, $disableDoctypes = false, $disableExterna * @return mixed */ protected static function recursiveXMLToArray($xml) { - if(is_object($xml) && get_class($xml) == 'SimpleXMLElement') { + if($xml instanceof SimpleXMLElement) { $attributes = $xml->attributes(); foreach($attributes as $k => $v) { if($v) $a[$k] = (string) $v; diff --git a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md index 8e9a2edb45f..8d713796434 100644 --- a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md +++ b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md @@ -145,3 +145,16 @@ html. To ensure that the correct encoding is used for that field in a template, `$Field` by itself to allow the casting helper to determine the best encoding itself. +## Cast summary methods + +Certain subclasses of DBField also have additional summary or manipulations methods, each of +which can be chained in order to perform more complicated manipulations. + +For instance, The following class methods can be used in templates for the below types: + +Text / HTMLText methods: + +* `$Plain` Will convert any HTML to plain text version. For example, could be used for plain-text + version of emails. +* `$LimitSentences()` Will limit to the first `` sentences in the content. If called on + HTML content this will have all HTML stripped and converted to plain text. diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index abcee089382..1f5bed07aa2 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -50,6 +50,7 @@ html fields as `HTMLText(['whitelist=meta,link'])`, or use a `ShortcodeHTMLText` as a shorthand substitute. * `FormField->dontEscape` has been removed. Escaping is now managed on a class by class basis. + * `DBString->LimitWordCountXML` removed. Use `LimitWordCount` for XML safe version. ## New API @@ -104,6 +105,11 @@ * `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions * `DataObject.table_name` config can now be used to customise the database table for any record. * `DataObjectSchema` class added to assist with mapping between classes and tables. + * Changes to `DBString` formatting: + * `NoHTML` is renamed to `Plain` + * `LimitWordCountXML` is removed. Use `LimitWordCount` instead. + * `BigSummary` is removed. Use `Summary` instead. + * Most limit methods on `DBHTMLText` now plain text rather than attempt to manipulate the underlying HTML. * `FormField::Title` and `FormField::RightTitle` are now cast as plain text by default (but can be overridden). ### Front-end build tooling for CMS interface @@ -174,7 +180,7 @@ admin/font/ => admin/client/dist/font/ * History.js * `debugmethods` querystring argument has been removed from debugging. - + * The following ClassInfo methods are now deprecated: * `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead. * `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead diff --git a/forms/gridfield/GridFieldLevelup.php b/forms/gridfield/GridFieldLevelup.php index b8e107385a3..34149217b62 100644 --- a/forms/gridfield/GridFieldLevelup.php +++ b/forms/gridfield/GridFieldLevelup.php @@ -1,6 +1,8 @@ $v) $attrsStr .= " $k=\"" . Convert::raw2att($v) . "\""; $forTemplate = new ArrayData(array( - 'UpLink' => sprintf('', $attrsStr) + 'UpLink' => DBField::create_field('HTMLFragment', sprintf('', $attrsStr)) )); return array( diff --git a/parsers/BBCodeParser.php b/parsers/BBCodeParser.php index 03c6f47b1bb..bc5855bb0b8 100644 --- a/parsers/BBCodeParser.php +++ b/parsers/BBCodeParser.php @@ -105,7 +105,7 @@ public static function usable_tags() { ); } - public function useable_tagsHTML(){ + public function useable_tagsHTML() { $useabletags = "