Merge branch '3.4' into 3

This commit is contained in:
Daniel Hensby 2016-07-04 23:54:27 +01:00
commit a449045b09
No known key found for this signature in database
GPG Key ID: 229831A941962E26
11 changed files with 235 additions and 53 deletions

View File

@ -221,19 +221,19 @@ abstract class DBField extends ViewableData {
} }
public function HTMLATT() { public function HTMLATT() {
return Convert::raw2htmlatt($this->value); return Convert::raw2htmlatt($this->RAW());
} }
public function URLATT() { public function URLATT() {
return urlencode($this->value); return urlencode($this->RAW());
} }
public function RAWURLATT() { public function RAWURLATT() {
return rawurlencode($this->value); return rawurlencode($this->RAW());
} }
public function ATT() { public function ATT() {
return Convert::raw2att($this->value); return Convert::raw2att($this->RAW());
} }
public function RAW() { public function RAW() {
@ -241,7 +241,7 @@ abstract class DBField extends ViewableData {
} }
public function JS() { public function JS() {
return Convert::raw2js($this->value); return Convert::raw2js($this->RAW());
} }
/** /**
@ -249,15 +249,15 @@ abstract class DBField extends ViewableData {
* @return string * @return string
*/ */
public function JSON() { public function JSON() {
return Convert::raw2json($this->value); return Convert::raw2json($this->RAW());
} }
public function HTML(){ public function HTML(){
return Convert::raw2xml($this->value); return Convert::raw2xml($this->RAW());
} }
public function XML(){ public function XML(){
return Convert::raw2xml($this->value); return Convert::raw2xml($this->RAW());
} }
/** /**

View File

@ -100,7 +100,7 @@ class HTMLText extends Text {
// Catch warnings thrown by loadHTML and turn them into a failure boolean rather than a SilverStripe error // 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); 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 // Nonbreaking spaces get converted into weird characters, so strip them
$value = str_replace(' ', ' ', $this->value); $value = str_replace(' ', ' ', $this->RAW());
try { try {
$res = $doc->loadHTML('<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>' . $value); $res = $doc->loadHTML('<meta content="text/html; charset=utf-8" http-equiv="Content-type"/>' . $value);
} }
@ -175,6 +175,15 @@ class HTMLText extends Text {
return $this->Summary(); return $this->Summary();
} }
public function RAW() {
if ($this->processShortcodes) {
return ShortcodeParser::get_active()->parse($this->value);
}
else {
return $this->value;
}
}
/** /**
* Return the value of the field with relative links converted to absolute urls (with placeholders parsed). * Return the value of the field with relative links converted to absolute urls (with placeholders parsed).
* @return string * @return string
@ -184,12 +193,7 @@ class HTMLText extends Text {
} }
public function forTemplate() { public function forTemplate() {
if ($this->processShortcodes) { return $this->RAW();
return ShortcodeParser::get_active()->parse($this->value);
}
else {
return $this->value;
}
} }
public function prepValueForDB($value) { public function prepValueForDB($value) {
@ -237,14 +241,16 @@ class HTMLText extends Text {
return false; return false;
} }
$value = $this->RAW();
// If it's got a content tag // If it's got a content tag
if(preg_match('/<(img|embed|object|iframe|meta|source|link)[^>]*>/i', $this->value)) { if(preg_match('/<(img|embed|object|iframe|meta|source|link)[^>]*>/i', $value)) {
return true; return true;
} }
// If it's just one or two tags on its own (and not the above) it's empty. // If it's just one or two tags on its own (and not the above) it's empty.
// This might be <p></p> or <h1></h1> or whatever. // This might be <p></p> or <h1></h1> or whatever.
if(preg_match('/^[\\s]*(<[^>]+>[\\s]*){1,2}$/', $this->value)) { if(preg_match('/^[\\s]*(<[^>]+>[\\s]*){1,2}$/', $value)) {
return false; return false;
} }

View File

@ -21,16 +21,21 @@ class HTMLVarchar extends Varchar {
} }
public function forTemplate() { public function forTemplate() {
return $this->RAW();
}
public function RAW() {
if ($this->processShortcodes) { if ($this->processShortcodes) {
return ShortcodeParser::get_active()->parse($this->value); return ShortcodeParser::get_active()->parse($this->value);
} }
else { else {
return $this->value; return $this->value;
} }
} }
public function exists() { public function exists() {
return parent::exists() && $this->value != '<p></p>'; return parent::exists() && $this->RAW() != '<p></p>';
} }
public function scaffoldFormField($title = null, $params = null) { public function scaffoldFormField($title = null, $params = null) {

View File

@ -84,9 +84,10 @@ abstract class StringField extends DBField {
* @see core/model/fieldtypes/DBField#exists() * @see core/model/fieldtypes/DBField#exists()
*/ */
public function exists() { public function exists() {
return $this->getValue() // All truthy values exist $value = $this->RAW();
|| (is_string($this->getValue()) && strlen($this->getValue())) // non-empty strings exist ('0' but not (int)0) return $value // All truthy values exist
|| (!$this->getNullifyEmpty() && $this->getValue() === ''); // Remove this stupid exemption in 4.0 || (is_string($value) && strlen($value)) // non-empty strings exist ('0' but not (int)0)
|| (!$this->getNullifyEmpty() && $value === ''); // Remove this stupid exemption in 4.0
} }
/** /**
@ -118,7 +119,7 @@ abstract class StringField extends DBField {
* @return string * @return string
*/ */
public function LimitCharacters($limit = 20, $add = '...') { public function LimitCharacters($limit = 20, $add = '...') {
$value = trim($this->value); $value = trim($this->RAW());
if($this->stat('escape_type') == 'xml') { if($this->stat('escape_type') == 'xml') {
$value = strip_tags($value); $value = strip_tags($value);
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
@ -142,13 +143,13 @@ abstract class StringField extends DBField {
*/ */
public function LimitCharactersToClosestWord($limit = 20, $add = '...') { public function LimitCharactersToClosestWord($limit = 20, $add = '...') {
// Strip HTML tags if they exist in the field // Strip HTML tags if they exist in the field
$this->value = strip_tags($this->value); $value = strip_tags($this->RAW());
// Determine if value exceeds limit before limiting characters // Determine if value exceeds limit before limiting characters
$exceedsLimit = mb_strlen($this->value) > $limit; $exceedsLimit = mb_strlen($value) > $limit;
// Limit to character limit // Limit to character limit
$value = $this->LimitCharacters($limit, ''); $value = DBField::create_field(get_class($this), $value)->LimitCharacters($limit, '');
// If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis // If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis
if($exceedsLimit) { if($exceedsLimit) {
@ -174,11 +175,11 @@ abstract class StringField extends DBField {
* @return string * @return string
*/ */
public function LimitWordCount($numWords = 26, $add = '...') { public function LimitWordCount($numWords = 26, $add = '...') {
$this->value = trim(Convert::xml2raw($this->value)); $value = trim(Convert::xml2raw($this->RAW()));
$ret = explode(' ', $this->value, $numWords + 1); $ret = explode(' ', $value, $numWords + 1);
if(count($ret) <= $numWords - 1) { if(count($ret) <= $numWords - 1) {
$ret = $this->value; $ret = $value;
} else { } else {
array_pop($ret); array_pop($ret);
$ret = implode(' ', $ret) . $add; $ret = implode(' ', $ret) . $add;
@ -209,7 +210,7 @@ abstract class StringField extends DBField {
* @return string * @return string
*/ */
public function LowerCase() { public function LowerCase() {
return mb_strtolower($this->value); return mb_strtolower($this->RAW());
} }
/** /**
@ -217,7 +218,7 @@ abstract class StringField extends DBField {
* @return string * @return string
*/ */
public function UpperCase() { public function UpperCase() {
return mb_strtoupper($this->value); return mb_strtoupper($this->RAW());
} }
/** /**
@ -226,6 +227,6 @@ abstract class StringField extends DBField {
* @return string * @return string
*/ */
public function NoHTML() { public function NoHTML() {
return strip_tags($this->value); return strip_tags($this->RAW());
} }
} }

View File

@ -60,7 +60,7 @@ class Text extends StringField {
* @return string * @return string
*/ */
public function AbsoluteLinks() { public function AbsoluteLinks() {
return HTTP::absoluteURLs($this->value); return HTTP::absoluteURLs($this->RAW());
} }
/** /**
@ -74,7 +74,7 @@ class Text extends StringField {
} }
$output = array(); $output = array();
$data = trim(Convert::xml2raw($this->value)); $data = trim(Convert::xml2raw($this->RAW()));
$sentences = explode('.', $data); $sentences = explode('.', $data);
if ($sentCount == 0) return ''; if ($sentCount == 0) return '';
@ -94,7 +94,7 @@ class Text extends StringField {
* Caution: Not XML/HTML-safe - does not respect closing tags. * Caution: Not XML/HTML-safe - does not respect closing tags.
*/ */
public function FirstSentence() { public function FirstSentence() {
$paragraph = Convert::xml2raw( $this->value ); $paragraph = Convert::xml2raw( $this->RAW() );
if( !$paragraph ) return ""; if( !$paragraph ) return "";
$words = preg_split('/\s+/', $paragraph); $words = preg_split('/\s+/', $paragraph);
@ -115,7 +115,7 @@ class Text extends StringField {
public function Summary($maxWords = 50) { public function Summary($maxWords = 50) {
// get first sentence? // get first sentence?
// this needs to be more robust // this needs to be more robust
$value = Convert::xml2raw( $this->value /*, true*/ ); $value = Convert::xml2raw( $this->RAW() /*, true*/ );
if(!$value) return ''; if(!$value) return '';
// grab the first paragraph, or, failing that, the whole content // grab the first paragraph, or, failing that, the whole content
@ -157,7 +157,7 @@ class Text extends StringField {
// get first sentence? // get first sentence?
// this needs to be more robust // this needs to be more robust
$data = $plain ? Convert::xml2raw($this->value, true) : $this->value; $data = $plain ? Convert::xml2raw($this->RAW(), true) : $this->RAW();
if(!$data) return ''; if(!$data) return '';
@ -197,8 +197,9 @@ class Text extends StringField {
public function FirstParagraph($plain = 1) { public function FirstParagraph($plain = 1) {
// get first sentence? // get first sentence?
// this needs to be more robust // this needs to be more robust
$value = $this->RAW();
if($plain && $plain != 'html') { if($plain && $plain != 'html') {
$data = Convert::xml2raw($this->value, true); $data = Convert::xml2raw($value, true);
if(!$data) return ""; if(!$data) return "";
// grab the first paragraph, or, failing that, the whole content // grab the first paragraph, or, failing that, the whole content
@ -207,12 +208,12 @@ class Text extends StringField {
return $data; return $data;
} else { } else {
if(strpos($this->value, "</p>") === false) return $this->value; if(strpos($value, "</p>") === false) return $value;
$data = substr($this->value, 0, strpos($this->value, "</p>") + 4); $data = substr($value, 0, strpos($value, "</p>") + 4);
if(strlen($data) < 20 && strpos($this->value, "</p>", strlen($data))) { if(strlen($data) < 20 && strpos($value, "</p>", strlen($data))) {
$data = substr($this->value, 0, strpos( $this->value, "</p>", strlen($data)) + 4 ); $data = substr($value, 0, strpos( $value, "</p>", strlen($data)) + 4 );
} }
return $data; return $data;
@ -241,7 +242,7 @@ class Text extends StringField {
} }
// Remove HTML tags so we don't have to deal with matching tags // Remove HTML tags so we don't have to deal with matching tags
$text = $striphtml ? $this->NoHTML() : $this->value; $text = $striphtml ? $this->NoHTML() : $this->RAW();
// Find the search string // Find the search string
$position = (int) stripos($text, $string); $position = (int) stripos($text, $string);
@ -273,7 +274,7 @@ class Text extends StringField {
$summary = trim($summary); $summary = trim($summary);
if($position > 0) $summary = $prefix . $summary; if($position > 0) $summary = $prefix . $summary;
if(strlen($this->value) > ($characters + $position)) $summary = $summary . $suffix; if(strlen($this->RAW()) > ($characters + $position)) $summary = $summary . $suffix;
return $summary; return $summary;
} }
@ -286,14 +287,14 @@ class Text extends StringField {
*/ */
public function Parse($parser = "TextParser") { public function Parse($parser = "TextParser") {
if($parser == "TextParser" || is_subclass_of($parser, "TextParser")) { if($parser == "TextParser" || is_subclass_of($parser, "TextParser")) {
$obj = new $parser($this->value); $obj = new $parser($this->RAW());
return $obj->parse(); return $obj->parse();
} else { } else {
// Fallback to using raw2xml and show a warning // Fallback to using raw2xml and show a warning
// TODO Don't kill script execution, we can continue without losing complete control of the app // TODO Don't kill script execution, we can continue without losing complete control of the app
user_error("Couldn't find an appropriate TextParser sub-class to create (Looked for '$parser')." user_error("Couldn't find an appropriate TextParser sub-class to create (Looked for '$parser')."
. "Make sure it sub-classes TextParser and that you've done ?flush=1.", E_USER_WARNING); . "Make sure it sub-classes TextParser and that you've done ?flush=1.", E_USER_WARNING);
return Convert::raw2xml($this->value); return Convert::raw2xml($this->RAW());
} }
} }

View File

@ -73,15 +73,19 @@ class Varchar extends StringField {
* Return the first letter of the string followed by a . * Return the first letter of the string followed by a .
*/ */
public function Initial() { public function Initial() {
if($this->exists()) return $this->value[0] . '.'; if($this->exists()) {
$value = $this->RAW();
return $value[0] . '.';
}
} }
/** /**
* Ensure that the given value is an absolute URL. * Ensure that the given value is an absolute URL.
*/ */
public function URL() { public function URL() {
if(preg_match('#^[a-zA-Z]+://#', $this->value)) return $this->value; $value = $this->RAW();
else return "http://" . $this->value; if(preg_match('#^[a-zA-Z]+://#', $value)) return $value;
else return "http://" . $value;
} }
/** /**
@ -89,7 +93,7 @@ class Varchar extends StringField {
* @return string * @return string
*/ */
public function RTF() { public function RTF() {
return str_replace("\n", '\par ', $this->value); return str_replace("\n", '\par ', $this->RAW());
} }
/** /**

View File

@ -256,9 +256,18 @@ class ShortcodeParser extends Object {
preg_match_all(static::attrrx(), $match['attrs'][0], $attrmatches, PREG_SET_ORDER); preg_match_all(static::attrrx(), $match['attrs'][0], $attrmatches, PREG_SET_ORDER);
foreach ($attrmatches as $attr) { foreach ($attrmatches as $attr) {
list($whole, $name, $value) = array_values(array_filter($attr)); $name = '';
$value = '';
$parts = array_values(array_filter($attr));
//the first element in the array is the complete delcaration (`id=1`) - we don't need this
array_shift($parts);
//the next two parts are what we care about (id and 1 from `id=1`)
$name = array_shift($parts) ?: $name;
$value = array_shift($parts) ?: $value;
$attrs[$name] = $value; $attrs[$name] = $value;
} }
} }
// And store the indexes, tag details, etc // And store the indexes, tag details, etc
@ -537,6 +546,9 @@ class ShortcodeParser extends Object {
// If no content, don't try and parse it // If no content, don't try and parse it
if (!trim($content)) return $content; if (!trim($content)) return $content;
// If no shortcode tag, don't try and parse it
if (strpos($content, '[') === false) return $content;
// First we operate in text mode, replacing any shortcodes with marker elements so that later we can // First we operate in text mode, replacing any shortcodes with marker elements so that later we can
// use a proper DOM // use a proper DOM
list($content, $tags) = $this->replaceElementTagsWithMarkers($content); list($content, $tags) = $this->replaceElementTagsWithMarkers($content);

View File

@ -185,10 +185,9 @@ class Permission extends DataObject implements TemplateGlobalProvider {
} }
} }
} }
elseif (substr($permCode, 0, 11) === 'CMS_ACCESS_') { elseif (substr($permCode, 0, 11) === 'CMS_ACCESS_' && !in_array('CMS_ACCESS_LeftAndMain', $code)) {
//cms_access_leftandmain means access to all CMS areas //cms_access_leftandmain means access to all CMS areas
$code[] = 'CMS_ACCESS_LeftAndMain'; $code[] = 'CMS_ACCESS_LeftAndMain';
break;
} }
} }

View File

@ -192,4 +192,126 @@ class HTMLTextTest extends SapphireTest {
'Removes any elements not in whitelist including text elements' 'Removes any elements not in whitelist including text elements'
); );
} }
public function testShortCodeParsedInRAW() {
$parser = ShortcodeParser::get('HTMLTextTest');
$parser->register('shortcode', function($arguments, $content, $parser, $tagName, $extra) {
return 'replaced';
});
ShortcodeParser::set_active('HTMLTextTest');
/** @var HTMLText $field */
$field = DBField::create_field('HTMLText', '<p>[shortcode]</p>');
$this->assertEquals('<p>replaced</p>', $field->RAW());
$this->assertEquals('<p>replaced</p>', (string)$field);
$field->setOptions(array(
'shortcodes' => false,
));
$this->assertEquals('<p>[shortcode]</p>', $field->RAW());
$this->assertEquals('<p>[shortcode]</p>', (string)$field);
ShortcodeParser::set_active('default');
}
public function testShortCodeParsedInTemplateHelpers() {
$parser = ShortcodeParser::get('HTMLTextTest');
$parser->register('shortcode', function($arguments, $content, $parser, $tagName, $extra) {
return 'Replaced short code with this. <a href="home">home</a>';
});
ShortcodeParser::set_active('HTMLTextTest');
/** @var HTMLText $field */
$field = DBField::create_field('HTMLText', '<p>[shortcode]</p>');
$this->assertEquals(
'&lt;p&gt;Replaced short code with this. &lt;a href=&quot;home&quot;&gt;home&lt;/a&gt;&lt;/p&gt;',
$field->HTMLATT()
);
$this->assertEquals(
'%3Cp%3EReplaced+short+code+with+this.+%3Ca+href%3D%22home%22%3Ehome%3C%2Fa%3E%3C%2Fp%3E',
$field->URLATT()
);
$this->assertEquals(
'%3Cp%3EReplaced%20short%20code%20with%20this.%20%3Ca%20href%3D%22home%22%3Ehome%3C%2Fa%3E%3C%2Fp%3E',
$field->RAWURLATT()
);
$this->assertEquals(
'&lt;p&gt;Replaced short code with this. &lt;a href=&quot;home&quot;&gt;home&lt;/a&gt;&lt;/p&gt;',
$field->ATT()
);
$this->assertEquals(
'<p>Replaced short code with this. <a href="home">home</a></p>',
$field->RAW()
);
$this->assertEquals(
'\x3cp\x3eReplaced short code with this. \x3ca href=\"home\"\x3ehome\x3c/a\x3e\x3c/p\x3e',
$field->JS()
);
$this->assertEquals(
'&lt;p&gt;Replaced short code with this. &lt;a href=&quot;home&quot;&gt;home&lt;/a&gt;&lt;/p&gt;',
$field->HTML()
);
$this->assertEquals(
'&lt;p&gt;Replaced short code with this. &lt;a href=&quot;home&quot;&gt;home&lt;/a&gt;&lt;/p&gt;',
$field->XML()
);
$this->assertEquals(
'Repl...',
$field->LimitCharacters(4, '...')
);
$this->assertEquals(
'Replaced...',
$field->LimitCharactersToClosestWord(10, '...')
);
$this->assertEquals(
'Replaced...',
$field->LimitWordCount(1, '...')
);
$this->assertEquals(
'<p>replaced short code with this. <a href="home">home</a></p>',
$field->LowerCase()
);
$this->assertEquals(
'<P>REPLACED SHORT CODE WITH THIS. <A HREF="HOME">HOME</A></P>',
$field->UpperCase()
);
$this->assertEquals(
'Replaced short code with this. home',
$field->NoHTML()
);
Config::nest();
Config::inst()->update('Director', 'alternate_base_url', 'http://example.com/');
$this->assertEquals(
'<p>Replaced short code with this. <a href="http://example.com/home">home</a></p>',
$field->AbsoluteLinks()
);
Config::unnest();
$this->assertEquals(
'Replaced short code with this.',
$field->LimitSentences(1)
);
$this->assertEquals(
'Replaced short code with this.',
$field->FirstSentence()
);
$this->assertEquals(
'Replaced short...',
$field->Summary(2)
);
$this->assertEquals(
'Replaced short code with...',
$field->BigSummary(4)
);
$this->assertEquals(
'Replaced short code with this. home[home]',
$field->FirstParagraph()
);
$this->assertEquals(
'Replaced <span class="highlight">short</span> <span class="highlight">code</span> with this. home',
$field->ContextSummary(500, 'short code')
);
ShortcodeParser::set_active('default');
}
} }

View File

@ -15,6 +15,12 @@ class ShortcodeParserTest extends SapphireTest {
parent::setUp(); parent::setUp();
} }
public function tearDown() {
ShortcodeParser::get('test')->unregister('test_shortcode');
parent::tearDown();
}
/** /**
* Tests that valid short codes that have not been registered are not replaced. * Tests that valid short codes that have not been registered are not replaced.
*/ */
@ -218,6 +224,14 @@ class ShortcodeParserTest extends SapphireTest {
); );
} }
public function testFalseyArguments() {
$this->parser->parse('<p>[test_shortcode falsey=0]');
$this->assertEquals(array(
'falsey' => '',
), $this->arguments);
}
public function testNumericShortcodes() { public function testNumericShortcodes() {
$this->assertEqualsIgnoringWhitespace( $this->assertEqualsIgnoringWhitespace(
'[2]', '[2]',
@ -240,6 +254,8 @@ class ShortcodeParserTest extends SapphireTest {
'<script>this is 2</script>', '<script>this is 2</script>',
$this->parser->parse('<script>[2]</script>') $this->parser->parse('<script>[2]</script>')
); );
$this->parser->unregister('2');
} }
public function testExtraContext() { public function testExtraContext() {
@ -250,6 +266,18 @@ class ShortcodeParserTest extends SapphireTest {
$this->assertEquals($this->extra['element']->tagName, 'a'); $this->assertEquals($this->extra['element']->tagName, 'a');
} }
public function testNoParseAttemptIfNoCode() {
$stub = $this->getMock('ShortcodeParser', array('replaceElementTagsWithMarkers'));
$stub->register('test', function() {
return '';
});
$stub->expects($this->never())
->method('replaceElementTagsWithMarkers')->will($this->returnValue(array('', '')));
$stub->parse('<p>test</p>');
}
// ----------------------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------------------
/** /**

View File

@ -27,6 +27,8 @@ class PermissionTest extends SapphireTest {
$members = Member::get()->byIDs($this->allFixtureIDs('Member')); $members = Member::get()->byIDs($this->allFixtureIDs('Member'));
foreach ($members as $member) { foreach ($members as $member) {
$this->assertTrue(Permission::checkMember($member, 'CMS_ACCESS')); $this->assertTrue(Permission::checkMember($member, 'CMS_ACCESS'));
$this->assertTrue(Permission::checkMember($member, array('CMS_ACCESS', 'CMS_ACCESS_Security')));
$this->assertTrue(Permission::checkMember($member, array('CMS_ACCESS_Security', 'CMS_ACCESS')));
} }
$member = new Member(); $member = new Member();
@ -37,6 +39,8 @@ class PermissionTest extends SapphireTest {
)); ));
$member->write(); $member->write();
$this->assertFalse(Permission::checkMember($member, 'CMS_ACCESS')); $this->assertFalse(Permission::checkMember($member, 'CMS_ACCESS'));
$this->assertFalse(Permission::checkMember($member, array('CMS_ACCESS', 'CMS_ACCESS_Security')));
$this->assertFalse(Permission::checkMember($member, array('CMS_ACCESS_Security', 'CMS_ACCESS')));
} }
public function testLeftAndMainAccessAll() { public function testLeftAndMainAccessAll() {