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('{?[a-zA-Z]+[^<>]*>}', '', $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('/\".$this->content."
"; - - $this->content = preg_replace('/(]*>)\s+/i', '\\1', $this->content); - $this->content = preg_replace('/\s+(<\/p[^>]*>)/i', '\\1', $this->content); - - $this->content = preg_replace("/\n\s*\n/", "
", $this->content);
- $this->content = str_replace("\n", "
", $this->content);
-
if($this->config()->allow_smilies) {
$smilies = array(
'#(? " ", // :D
diff --git a/parsers/TextParser.php b/parsers/TextParser.php
index 97ed2d68510..0f4bd578c90 100644
--- a/parsers/TextParser.php
+++ b/parsers/TextParser.php
@@ -1,4 +1,6 @@
üåäö&ÜÅÄÖ
'; foreach ($htmlFields as $stringField) { - $stringField = DBField::create_field($stringField, $value); - $this->assertEquals('üåäö&ÜÅÄ...', $stringField->LimitCharacters(8)); + $stringObj = DBField::create_field($stringField, $value); + + // Converted to plain text + $this->assertEquals('üåäö&ÜÅÄ...', $stringObj->LimitCharacters(8)); + + // But which will be safely cast in templates + $this->assertEquals('üåäö&ÜÅÄ...', $stringObj->obj('LimitCharacters', [8])->forTemplate()); } $this->assertEquals('ÅÄÖ', DBField::create_field('Text', 'åäö')->UpperCase()); $this->assertEquals('åäö', DBField::create_field('Text', 'ÅÄÖ')->LowerCase()); + $this->assertEquals('ÅÄÖ
', DBField::create_field('HTMLFragment', 'åäö
')->UpperCase()); + $this->assertEquals('åäö
', DBField::create_field('HTMLFragment', 'ÅÄÖ
')->LowerCase()); } } diff --git a/tests/model/DBHTMLTextTest.php b/tests/model/DBHTMLTextTest.php index 35e4c0ae943..7f83db34279 100644 --- a/tests/model/DBHTMLTextTest.php +++ b/tests/model/DBHTMLTextTest.php @@ -12,142 +12,385 @@ */ class DBHTMLTextTest extends SapphireTest { + public function setUp() { + parent::setUp(); + + // Set test handler + ShortcodeParser::get('htmltest') + ->register('test_shortcode', array('DBHTMLTextTest_Shortcode', 'handle_shortcode')); + ShortcodeParser::set_active('htmltest'); + } + + public function tearDown() { + ShortcodeParser::set_active('default'); + parent::tearDown(); + } + /** - * Test {@link HTMLText->LimitCharacters()} + * Test {@link Text->LimitCharacters()} */ - public function testLimitCharacters() { - $cases = array( - 'The little brown fox jumped over the lazy cow.' => 'The little brown fox...', - 'This is some text in a paragraph.
' => 'This is some text in...', - 'This text contains & in it' => 'This text contains &...' - ); + public function providerLimitCharacters() + { + // HTML characters are stripped safely + return [ + ['The little brown fox jumped over the lazy cow.', 'The little brown fox...'], + ['Short & Sweet
', 'Short & Sweet'], + ['This text contains & in it', 'This text contains &...'], + ]; + } - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBHTMLText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->LimitCharacters()); - } + /** + * Test {@link DBHTMLText->LimitCharacters()} + * @dataProvider providerLimitCharacters + * @param string $originalValue + * @param string $expectedValue + */ + public function testLimitCharacters($originalValue, $expectedValue) { + $textObj = DBField::create_field('HTMLFragment', $originalValue); + $result = $textObj->obj('LimitCharacters')->forTemplate(); + $this->assertEquals($expectedValue, $result); } - public function testSummaryBasics() { - $cases = array( - 'Should take paragraph
' => 'Should take paragraph', - 'Should strip tags, but leave text
' => 'Should strip tags, but leave text', - 'Unclosed tags
should not phase it
Second paragraph
should not cause errors or appear in output
' => 'Second paragraph', - 'Second paragraph
should not cause errors or appear in output
' - => 'Second paragraph', - 'Second paragraph
should not cause errors or appear in output
' - => 'Second paragraph', - 'example text words hello
' - => 'example text words hello', - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBHTMLText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->Summary()); - } + /** + * @return array + */ + public function providerLimitCharactersToClosestWord() + { + // HTML is converted safely to plain text + return [ + // Standard words limited, ellipsis added if truncated + ['Lorem ipsum dolor sit amet
', 24, 'Lorem ipsum dolor sit...'], + + // Complete words less than the character limit don't get truncated, ellipsis not added + ['Lorem ipsum
', 24, 'Lorem ipsum'], + ['Lorem
', 24, 'Lorem'], + ['', 24, ''], // No words produces nothing! + + // Special characters are encoded safely + ['Nice & Easy', 24, 'Nice & Easy'], + + // HTML is safely converted to plain text + ['Lorem ipsum dolor sit amet
', 24, 'Lorem ipsum dolor sit...'], + ['Lorem ipsum dolor sit amet
', 24, 'Lorem ipsum dolor sit...'], + ['Lorem ipsum
', 24, 'Lorem ipsum'], + ['Lorem & ipsum dolor sit amet', 24, 'Lorem & ipsum dolor sit...'] + ]; } - public function testSummaryLimits() { - $cases = array( - 'A long paragraph should be cut off if limit is set
' => 'A long paragraph should be...', - 'No matter how many tags are in it
' => 'No matter how many tags...', - 'A sentence is. nicer than hard limits
' => 'A sentence is.', - 'But not. If it\'s too short
' => 'But not. If it\'s too...' - ); + /** + * Test {@link DBHTMLText->LimitCharactersToClosestWord()} + * @dataProvider providerLimitCharactersToClosestWord + * + * @param string $originalValue Raw string input + * @param int $limit + * @param string $expectedValue Expected template value + */ + public function testLimitCharactersToClosestWord($originalValue, $limit, $expectedValue) { + $textObj = DBField::create_field('HTMLFragment', $originalValue); + $result = $textObj->obj('LimitCharactersToClosestWord', [$limit])->forTemplate(); + $this->assertEquals($expectedValue, $result); + } - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBHTMLText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->Summary(5, 3, '...')); - } + public function providerSummary() + { + return [ + [ + 'Should strip tags, but leave text
', + 50, + 'Should strip tags, but leave text', + ], + [ + // Line breaks are preserved + 'Unclosed tags
should not phase it
Second paragraph
should not cause errors or appear in output
', + 50, + "Second paragraphSecond paragraph
should not cause errors or appear in output
', + 50, + "Second paragraphSecond paragraph
should not cause errors or appear in output
', + 50, + "Second paragraphexample text words hello
', + 50, + 'example text words hello', + ], + + // Shorter limits + [ + 'A long paragraph should be cut off if limit is set
', + 5, + 'A long paragraph should be...', + ], + [ + 'No matter how many tags are in it
', + 5, + 'No matter how many tags...', + ], + [ + 'A sentence is. nicer than hard limits
', + 5, + 'A sentence is.', + ], + ]; + } + + /** + * @dataProvider providerSummary + * @param string $originalValue + * @param int $limit + * @param string $expectedValue + */ + public function testSummary($originalValue, $limit, $expectedValue) { + $textObj = DBField::create_field('HTMLFragment', $originalValue); + $result = $textObj->obj('Summary', [$limit])->forTemplate(); + $this->assertEquals($expectedValue, $result); } public function testSummaryEndings() { $cases = array( - '...', ' -> more', '' + '...', + ' -> more', + '' ); $orig = 'Cut it off, cut it off
'; $match = 'Cut it off, cut'; foreach($cases as $add) { - $textObj = new DBHTMLText(); - $textObj->setValue($orig); - $this->assertEquals($match.$add, $textObj->Summary(4, 0, $add)); + $textObj = DBField::create_field('HTMLFragment', $orig); + $result = $textObj->obj('Summary', [4, $add])->forTemplate(); + $this->assertEquals($match.Convert::raw2xml($add), $result); } } - public function testSummaryFlexTooBigShouldNotCauseError() { - $orig = 'Cut it off, cut it off
'; - $match = 'Cut it off, cut'; - $textObj = new DBHTMLText(); - $textObj->setValue($orig); - $this->assertEquals($match, $textObj->Summary(4, 10, '')); + + public function providerFirstSentence() + { + return [ + // Same behaviour as DBTextTest + ['', ''], + ['First sentence.', 'First sentence.'], + ['First sentence. Second sentence', 'First sentence.'], + ['First sentence? Second sentence', 'First sentence?'], + ['First sentence! Second sentence', 'First sentence!'], + + // DBHTHLText strips HTML first + ['First sentence. Second sentence. Third sentence
', 'First sentence.'], + ]; } - public function testSummaryInvalidHTML() { - $cases = array( - 'It\'s got atag, but
This doesn\'t make < >
Excessive
Newlines
First sentence. Second sentence.
' => 'First sentence.', - 'First Mr. sentence. Second sentence.
' => 'First Mr. sentence.', - "Sentence with {$many}words. Second sentence.
" - => "Sentence with {$many}words.", - 'This classic picture book features a repetitive format that lends itself to audience interaction.'. - ' Illustrator Eric Carle submitted new, bolder artwork for the 25th anniversary edition.
' - => 'This classic picture book features a repetitive format that lends itself to audience interaction.' - ); - - foreach($cases as $orig => $match) { - $textObj = new DBHTMLText(); - $textObj->setValue($orig); - $this->assertEquals($match, $textObj->FirstSentence()); - } + /** + * @dataProvider providerToPlain + * @param string $html + * @param string $plain + */ + public function testToPlain($html, $plain) { + /** @var DBHTMLText $textObj */ + $textObj = DBField::create_field('HTMLFragment', $html); + $this->assertEquals($plain, $textObj->Plain()); + } + + /** + * each test is in the format input, charactere limit, highlight, expected output + * + * @return array + */ + public function providerContextSummary() + { + return [ + [ + 'This is some text. It is a test', + 20, + 'test', + '... text. It is a test' + ], + [ + // Retains case of original string + 'This is some test text. Test test what if you have multiple keywords.', + 50, + 'some test', + 'This is some test text.' + . ' Test test what if you have...' + ], + [ + 'Here is some text & HTML included', + 20, + 'html', + '... text & HTML inc...' + ], + [ + 'A dog ate a cat while looking at a Foobar', + 100, + 'a', + // test that it does not highlight too much (eg every a) + 'A dog ate a cat while looking at a Foobar', + ], + [ + 'A dog ate a cat while looking at a Foobar', + 100, + 'ate', + // it should highlight 3 letters or more. + 'A dog ate a cat while looking at a Foobar', + ], + + // HTML Content is plain-textified, and incorrect tags removed + [ + 'A dog ate a cat while looking at a Foobar
', + 100, + 'ate', + // it should highlight 3 letters or more. + 'A dog ate a cat while looking at a Foobar', + ] + ]; + } + + /** + * @dataProvider providerContextSummary + * @param string $originalValue Input + * @param int $limit Numer of characters + * @param string $keywords Keywords to highlight + * @param string $expectedValue Expected output (XML encoded safely) + */ + public function testContextSummary($originalValue, $limit, $keywords, $expectedValue) + { + $text = DBField::create_field('HTMLFragment', $originalValue); + $result = $text->obj('ContextSummary', [$limit, $keywords])->forTemplate(); + // it should highlight 3 letters or more. + $this->assertEquals($expectedValue, $result); } public function testRAW() { - $data = DBField::create_field('HTMLText', 'This & This'); - $this->assertEquals($data->RAW(), 'This & This'); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->RAW()); - $data = DBField::create_field('HTMLText', 'This & This'); - $this->assertEquals($data->RAW(), 'This & This'); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->RAW()); } public function testXML() { - $data = DBField::create_field('HTMLText', 'This & This'); - $this->assertEquals($data->XML(), 'This & This'); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->XML()); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->XML()); } public function testHTML() { - $data = DBField::create_field('HTMLText', 'This & This'); - $this->assertEquals($data->HTML(), 'This & This'); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->HTML()); + $data = DBField::create_field('HTMLFragment', 'This & This'); + $this->assertEquals('This & This', $data->HTML()); } public function testJS() { - $data = DBField::create_field('HTMLText', '"this is a test"'); - $this->assertEquals($data->JS(), '\"this is a test\"'); + $data = DBField::create_field('HTMLText', '"this is & test"'); + $this->assertEquals('\"this is \x26amp; test\"', $data->JS()); } public function testATT() { - $data = DBField::create_field('HTMLText', '"this is a test"'); - $this->assertEquals($data->ATT(), '"this is a test"'); + // HTML Fragment + $data = DBField::create_field('HTMLFragment', '"this is a test"'); + $this->assertEquals('"this is a test"', $data->ATT()); + + // HTML Text (passes shortcodes + tidy) + $data = DBField::create_field('HTMLText', '"'); + $this->assertEquals('"', $data->ATT()); + } + + public function testShortcodesProcessed() + { + /** @var DBHTMLText $obj */ + $obj = DBField::create_field( + 'HTMLText', + 'Some content [test_shortcode] with shortcode
' + ); + // Basic DBField methods process shortcodes + $this->assertEquals( + 'Some content shortcode content with shortcode', + $obj->Plain() + ); + $this->assertEquals( + 'Some content shortcode content with shortcode
', + $obj->RAW() + ); + $this->assertEquals( + '<p>Some content <strong>shortcode content</strong> with shortcode</p>', + $obj->XML() + ); + $this->assertEquals( + '<p>Some content <strong>shortcode content</strong> with shortcode</p>', + $obj->HTML() + ); + // Test summary methods + $this->assertEquals( + 'Some content shortcode...', + $obj->Summary(3) + ); + $this->assertEquals( + 'Some content shortcode content with shortcode', + $obj->LimitSentences(1) + ); + $this->assertEquals( + 'Some content shortco...', + $obj->LimitCharacters(20) + ); + } + + public function testParse() { + // Test parse + /** @var DBHTMLText $obj */ + $obj = DBField::create_field( + 'HTMLText', + '[b]Some content[/b] [test_shortcode] with shortcode
' + ); + + // BBCode strips HTML and applies own formatting + $this->assertEquals( + 'Some content shortcode content with shortcode', + $obj->Parse('BBCodeParser')->forTemplate() + ); } function testExists() { @@ -322,3 +565,15 @@ public function testShortCodeParsedInTemplateHelpers() { ShortcodeParser::set_active('default'); } } + +class DBHTMLTextTest_Shortcode implements ShortcodeHandler, TestOnly { + public static function get_shortcodes() + { + return 'test'; + } + + public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) + { + return 'shortcode content'; + } +} diff --git a/tests/model/DBTextTest.php b/tests/model/DBTextTest.php index 60ae31443cf..a0185ed8878 100644 --- a/tests/model/DBTextTest.php +++ b/tests/model/DBTextTest.php @@ -7,6 +7,8 @@ /** + * Tests parsing and summary methods on DBText + * * @package framework * @subpackage tests */ @@ -15,211 +17,224 @@ class DBTextTest extends SapphireTest { /** * Test {@link Text->LimitCharacters()} */ - public function testLimitCharacters() { - $cases = array( - 'The little brown fox jumped over the lazy cow.' => 'The little brown fox...', - 'This is some text in a paragraph.
' => 'This is some text...' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->LimitCharacters()); - } + public function providerLimitCharacters() + { + // Plain text values always encoded safely + // HTML stored in non-html fields is treated literally. + return [ + ['The little brown fox jumped over the lazy cow.', 'The little brown fox...'], + ['
Short & Sweet
', '<p>Short & Sweet</p>'], + ['This text contains & in it', 'This text contains &...'], + ]; } /** - * Test {@link Text->LimitCharactersToClosestWord()} + * Test {@link Text->LimitCharacters()} + * @dataProvider providerLimitCharacters + * @param string $originalValue + * @param string $expectedValue */ - public function testLimitCharactersToClosestWord() { - $cases = array( - /* Standard words limited, ellipsis added if truncated */ - 'Lorem ipsum dolor sit amet' => 'Lorem ipsum dolor sit...', - - /* Complete words less than the character limit don't get truncated, ellipsis not added */ - 'Lorem ipsum' => 'Lorem ipsum', - 'Lorem' => 'Lorem', - '' => '', // No words produces nothing! - - /* HTML tags get stripped out, leaving the raw text */ - 'Lorem ipsum dolor sit amet
' => 'Lorem ipsum dolor sit...', - 'Lorem ipsum dolor sit amet
' => 'Lorem ipsum dolor sit...', - 'Lorem ipsum
' => 'Lorem ipsum', - - /* HTML entities are treated as a single character */ - 'Lorem & ipsum dolor sit amet' => 'Lorem & ipsum dolor...' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->LimitCharactersToClosestWord(24)); - } + public function testLimitCharacters($originalValue, $expectedValue) { + $textObj = DBField::create_field('Text', $originalValue); + $result = $textObj->obj('LimitCharacters')->forTemplate(); + $this->assertEquals($expectedValue, $result); } /** - * Test {@link Text->LimitWordCount()} + * @return array */ - public function testLimitWordCount() { - $cases = array( - /* Standard words limited, ellipsis added if truncated */ - 'The little brown fox jumped over the lazy cow.' => 'The little brown...', - ' This text has white space around the ends ' => 'This text has...', - - /* Words less than the limt word count don't get truncated, ellipsis not added */ - 'Two words' => 'Two words', // Two words shouldn't have an ellipsis - 'One' => 'One', // Neither should one word - '' => '', // No words produces nothing! - - /* HTML tags get stripped out, leaving the raw text */ - 'Text inside a paragraph tag should also work
' => 'Text inside a...', - 'Text nested inside another tag should also work
' => 'Text nested inside...', - 'Two words
' => 'Two words' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->LimitWordCount(3)); - } + public function providerLimitCharactersToClosestWord() + { + return [ + // Standard words limited, ellipsis added if truncated + ['Lorem ipsum dolor sit amet', 24, 'Lorem ipsum dolor sit...'], + + // Complete words less than the character limit don't get truncated, ellipsis not added + ['Lorem ipsum', 24, 'Lorem ipsum'], + ['Lorem', 24, 'Lorem'], + ['', 24, ''], // No words produces nothing! + + // Special characters are encoded safely + ['Nice & Easy', 24, 'Nice & Easy'], + + // HTML stored in non-html fields is treated literally. + // If storing HTML you should use DBHTMLText instead + ['Lorem ipsum dolor sit amet
', 24, '<p>Lorem ipsum dolor...'], + ['Lorem ipsum dolor sit amet
', 24, '<p><span>Lorem ipsum...'], + ['Lorem ipsum
', 24, '<p>Lorem ipsum</p>'], + ['Lorem & ipsum dolor sit amet', 24, 'Lorem & ipsum dolor...'] + ]; } /** - * Test {@link Text->LimitWordCountXML()} + * Test {@link Text->LimitCharactersToClosestWord()} + * @dataProvider providerLimitCharactersToClosestWord + * + * @param string $originalValue Raw string input + * @param int $limit + * @param string $expectedValue Expected template value */ - public function testLimitWordCountXML() { - $cases = array( - 'Stuff & stuff
' => 'Stuff &...', - "Stuff\nBlah Blah Blah" => "Stuff\nBlah Blah...", - "StuffFirst sentence.
' => 'First sentence.', - 'First sentence. Second sentence. Third sentence
' => 'First sentence. Second sentence.', - 'First sentence. Second sentence. Third sentence
' => 'First sentence. Second sentence.', - 'First sentence. Second sentence. Third sentence
' - => 'First sentence. Second sentence.' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->LimitSentences(2)); - } - } - - public function testFirstSentance() { - $cases = array( - '' => '', - 'First sentence.' => 'First sentence.', - 'First sentence. Second sentence' => 'First sentence.', - 'First sentence? Second sentence' => 'First sentence?', - 'First sentence! Second sentence' => 'First sentence!', - 'First sentence.
' => 'First sentence.', - 'First sentence. Second sentence. Third sentence
' => 'First sentence.', - 'First sentence. Second sentence. Third sentence
' => 'First sentence.', - 'First sentence. Second sentence. Third sentence
' - => 'First sentence.' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = new DBText('Test'); - $textObj->setValue($originalValue); - $this->assertEquals($expectedValue, $textObj->FirstSentence()); - } + public function providerLimitWordCount() { + return [ + // Standard words limited, ellipsis added if truncated + ['The little brown fox jumped over the lazy cow.', 3, 'The little brown...'], + [' This text has white space around the ends ', 3, 'This text has...'], + + // Words less than the limt word count don't get truncated, ellipsis not added + ['Two words', 3, 'Two words'], // Two words shouldn't have an ellipsis + ['These three words', 3, 'These three words'], // Three words shouldn't have an ellipsis + ['One', 3, 'One'], // Neither should one word + ['', 3, ''], // No words produces nothing! + + // Text with special characters + ['Nice & Easy', 3, 'Nice & Easy'], + ['One & Two & Three', 3, 'One & Two...'], + + // HTML stored in non-html fields is treated literally. + // If storing HTML you should use DBHTMLText instead + ['Text inside a paragraph tag should also work
', 3, '<p>Text inside a...'], + ['Two words
', 3, '<p>Two words</p>'], + ]; } /** - * Test {@link Text->BigSummary()} + * Test {@link DBText->LimitWordCount()} + * @dataProvider providerLimitWordCount + * + * @param string $originalValue Raw string input + * @param int $limit Number of words + * @param string $expectedValue Expected template value */ - public function testBigSummaryPlain() { - $cases = array( - 'This text has multiple sentences. Big Summary uses this to split sentences up.
' - => 'This text has multiple...', - 'This text does not have multiple sentences' => 'This text does not...', - 'Very short' => 'Very short', - '' => '' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = DBField::create_field('Text', $originalValue); - $this->assertEquals($expectedValue, $textObj->BigSummary(4, true)); - } + public function testLimitWordCount($originalValue, $limit, $expectedValue) { + $textObj = DBField::create_field('Text', $originalValue); + $result = $textObj->obj('LimitWordCount', [$limit])->forTemplate(); + $this->assertEquals($expectedValue, $result); } /** - * Test {@link Text->BigSummary()} */ - public function testBigSummary() { - $cases = array( - 'This text has multiple sentences. Big Summary uses this to split sentences up.' - => 'This text has multiple...', - 'This text does not have multiple sentences' => 'This text does not...', - 'Very short' => 'Very short', - '' => '' - ); - - foreach($cases as $originalValue => $expectedValue) { - $textObj = DBField::create_field('Text', $originalValue); - $this->assertEquals($expectedValue, $textObj->BigSummary(4, false)); - } + public function providerLimitSentences() + { + return [ + ['', 2, ''], + ['First sentence.', 2, 'First sentence.'], + ['First sentence. Second sentence.', 2, 'First sentence. Second sentence.'], + + // HTML stored in non-html fields is treated literally. + // If storing HTML you should use DBHTMLText instead + ['First sentence.
', 2, '<p>First sentence.</p>'], + ['First sentence. Second sentence. Third sentence
', 2, '<p>First sentence. Second sentence.'], + ]; } - public function testContextSummary() { - $testString1 = 'This is some text. It is a test
'; - $testKeywords1 = 'test'; - - $testString2 = 'This is some test text. Test test what if you have multiple keywords.
'; - $testKeywords2 = 'some test'; - - $testString3 = 'A dog ate a cat while looking at a Foobar
'; - $testKeyword3 = 'a'; - $testKeyword3a = 'ate'; - - $textObj = DBField::create_field('Text', $testString1, 'Text'); - - $this->assertEquals( - '... text. It is a test...', - $textObj->ContextSummary(20, $testKeywords1) - ); - - $textObj->setValue($testString2); + /** + * Test {@link DBText->LimitSentences()} + * + * @dataProvider providerLimitSentences + * @param string $originalValue + * @param int $limit Number of sentences + * @param string $expectedValue Expected template value + */ + public function testLimitSentences($originalValue, $limit, $expectedValue) { + $textObj = DBField::create_field('Text', $originalValue); + $result = $textObj->obj('LimitSentences', [$limit])->forTemplate(); + $this->assertEquals($expectedValue, $result); + } - $this->assertEquals( - 'This is some test text.' - . ' test test what if you have...', - $textObj->ContextSummary(50, $testKeywords2) - ); + public function providerFirstSentence() + { + return [ + ['', ''], + ['First sentence.', 'First sentence.'], + ['First sentence. Second sentence', 'First sentence.'], + ['First sentence? Second sentence', 'First sentence?'], + ['First sentence! Second sentence', 'First sentence!'], + + // HTML stored in non-html fields is treated literally. + // If storing HTML you should use DBHTMLText instead + ['First sentence. Second sentence. Third sentence
', '<p>First sentence.'], + ]; + } - $textObj->setValue($testString3); + /** + * @dataProvider providerFirstSentence + * @param string $originalValue + * @param string $expectedValue + */ + public function testFirstSentence($originalValue, $expectedValue) { + $textObj = DBField::create_field('Text', $originalValue); + $result = $textObj->obj('FirstSentence')->forTemplate(); + $this->assertEquals($expectedValue, $result); + } - // test that it does not highlight too much (eg every a) - $this->assertEquals( - 'A dog ate a cat while looking at a Foobar', - $textObj->ContextSummary(100, $testKeyword3) - ); + /** + * each test is in the format input, charactere limit, highlight, expected output + * + * @return array + */ + public function providerContextSummary() + { + return [ + [ + 'This is some text. It is a test', + 20, + 'test', + '... text. It is a test' + ], + [ + // Retains case of original string + 'This is some test text. Test test what if you have multiple keywords.', + 50, + 'some test', + 'This is some test text.' + . ' Test test what if you have...' + ], + [ + 'Here is some text & HTML included', + 20, + 'html', + '... text & HTML inc...' + ], + [ + 'A dog ate a cat while looking at a Foobar', + 100, + 'a', + // test that it does not highlight too much (eg every a) + 'A dog ate a cat while looking at a Foobar', + ], + [ + 'A dog ate a cat while looking at a Foobar', + 100, + 'ate', + // it should highlight 3 letters or more. + 'A dog ate a cat while looking at a Foobar', + ] + ]; + } + /** + * @dataProvider providerContextSummary + * @param string $originalValue Input + * @param int $limit Numer of characters + * @param string $keywords Keywords to highlight + * @param string $expectedValue Expected output (XML encoded safely) + */ + public function testContextSummary($originalValue, $limit, $keywords, $expectedValue) + { + $text = DBField::create_field('Text', $originalValue); + $result = $text->obj('ContextSummary', [$limit, $keywords])->forTemplate(); // it should highlight 3 letters or more. - $this->assertEquals( - 'A dog ate a cat while looking at a Foobar', - $textObj->ContextSummary(100, $testKeyword3a) - ); + $this->assertEquals($expectedValue, $result); } public function testRAW() { diff --git a/tests/model/DatabaseTest.php b/tests/model/DatabaseTest.php index 237dbced86d..93c685fa242 100644 --- a/tests/model/DatabaseTest.php +++ b/tests/model/DatabaseTest.php @@ -159,7 +159,7 @@ public function testCanLock() { } public function testTransactions() { - $conn = DB::getConn(); + $conn = DB::get_conn(); if(!$conn->supportsTransactions()) { $this->markTestSkipped("DB Doesn't support transactions"); return; diff --git a/tests/view/ViewableDataTest.php b/tests/view/ViewableDataTest.php index 8a2ebe031f2..62a68d47d87 100644 --- a/tests/view/ViewableDataTest.php +++ b/tests/view/ViewableDataTest.php @@ -24,8 +24,8 @@ public function testCasting() { $this->assertEquals($htmlString, $htmlField->obj('ATT')->forTemplate()); $this->assertEquals($textString, $htmlField->obj('RAW')->forTemplate()); $this->assertEquals('\"', $htmlField->obj('JS')->forTemplate()); - $this->assertEquals($textString, $htmlField->obj('HTML')->forTemplate()); - $this->assertEquals($textString, $htmlField->obj('XML')->forTemplate()); + $this->assertEquals($htmlString, $htmlField->obj('HTML')->forTemplate()); + $this->assertEquals($htmlString, $htmlField->obj('XML')->forTemplate()); $textField = DBField::create_field('Text', $textString); $this->assertEquals($htmlString, $textField->forTemplate());