diff --git a/api/RestfulService.php b/api/RestfulService.php index 14ce54b59..7f007e9cc 100644 --- a/api/RestfulService.php +++ b/api/RestfulService.php @@ -9,11 +9,9 @@ class RestfulService extends ViewableData { protected $baseURL; protected $queryString; - protected $rawXML; protected $errorTag; protected $checkErrors; protected $cache_expire; - protected $authUsername, $authPassword; protected $customHeaders = array(); @@ -59,7 +57,7 @@ class RestfulService extends ViewableData { */ public function connect($subURL = '') { user_error("RestfulService::connect is deprecated; use RestfulService::request", E_USER_NOTICE); - return $this->request($subURL, 'GET'); + return $this->request($subURL)->getBody(); } /** @@ -72,7 +70,7 @@ class RestfulService extends ViewableData { * This is a replacement of {@link connect()}. */ public function request($subURL = '', $method = "GET", $data = null, $headers = null) { - $url = $this->baseURL . $subURL; //url for the request + $url = $this->baseURL . $subURL; // Url for the request if($this->queryString) { if(strpos($url, '?') !== false) { $url .= '&' . $this->queryString; @@ -80,68 +78,68 @@ class RestfulService extends ViewableData { $url .= '?' . $this->queryString; } } - $url = str_replace(' ', '%20', $url); // spaces should be encoded + $url = str_replace(' ', '%20', $url); // Encode spaces $method = strtoupper($method); assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS'))); - //check for file exists in cache - //set the cache directory + $cachedir = TEMP_FOLDER; // Default silverstripe cache + $cache_file = md5($url); // Encoded name of cache file + $cache_path = $cachedir."/xmlresponse_$cache_file"; - /* we've disabled caching until we can figure out how to deal with storing responses - $cachedir=TEMP_FOLDER; //default silverstrip-cache - $cache_file = md5($url); //encoded name of cache file - $cache_path = $cachedir."/$cache_file"; - - if( !isset($_GET['flush']) && ( @file_exists("$cache_path") && ((@filemtime($cache_path) + $this->cache_expire) > ( time() )))){ - $this->rawXML = file_get_contents($cache_path); + // Check for unexpired cached feed (unless flush is set) + if(!isset($_GET['flush']) && @file_exists($cache_path) && @filemtime($cache_path) + $this->cache_expire > time()) { + $store = file_get_contents($cache_path); + $response = unserialize($store); - } else {//not available in cache fetch from server - */ - $ch = curl_init(); - $timeout = 5; - $useragent = "SilverStripe/2.2"; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_USERAGENT, $useragent); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + } else { + $ch = curl_init(); + $timeout = 5; + $useragent = "SilverStripe/2.2"; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if($this->customHeaders) { - $headers = array_merge((array)$this->customHeaders, (array)$headers); - } - - if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword"); - - if($method == 'POST') { - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - } - - $responseBody = curl_exec($ch); - - if($responseBody === false) { - $curlError = curl_error($ch); - // Problem verifying the server SSL certificate; just ignore it as it's not mandatory - if(strpos($curlError,'14090086') !== false) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $responseBody = curl_exec($ch); - $curlError = curl_error($ch); + // Add headers + if($this->customHeaders) { + $headers = array_merge((array)$this->customHeaders, (array)$headers); } - - if($respnoseBody === false) { + + if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Add authentication + if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword"); + + // Add fields to POST requests + if($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + + $responseBody = curl_exec($ch); + + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $responseBody = curl_exec($ch); + $curlError = curl_error($ch); + + if($curlError) { user_error("Curl Error:" . $curlError, E_USER_WARNING); return; } + + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $response = new RestfulService_Response($responseBody, curl_getinfo($ch, CURLINFO_HTTP_CODE)); + + curl_close($ch); + + // Serialise response object and write to cache + $store = serialize($response); + file_put_contents($cache_path,$store); } - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $response = new RestfulService_Response($responseBody, curl_getinfo($ch, CURLINFO_HTTP_CODE)); - - curl_close($ch); - return $response; } @@ -164,14 +162,13 @@ class RestfulService extends ViewableData { $childElements = $xml->{$collection}->{$element}; if($childElements){ - foreach($childElements as $child){ - $data = array(); - foreach($child->attributes() as $key => $value){ - $data["$key"] = Convert::raw2xml($value); + foreach($childElements as $child){ + $data = array(); + foreach($child->attributes() as $key => $value){ + $data["$key"] = Convert::raw2xml($value); + } + $output->push(new ArrayData($data)); } - - $output->push(new ArrayData($data)); - } } return $output; diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php index d5b87e049..150ba31c8 100644 --- a/api/XMLDataFormatter.php +++ b/api/XMLDataFormatter.php @@ -39,7 +39,7 @@ class XMLDataFormatter extends DataFormatter { $id = $obj->ID; $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID"); - $json = "<$className href=\"$objHref.xml\">\n"; + $xml = "<$className href=\"$objHref.xml\">\n"; foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) { // Field filtering if($fields && !in_array($fieldName, $fields)) continue; @@ -48,9 +48,9 @@ class XMLDataFormatter extends DataFormatter { if(!mb_check_encoding($fieldValue,'utf-8')) $fieldValue = "(data is badly encoded)"; if(is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toXML')) { - $json .= $fieldValue->toXML(); + $xml .= $fieldValue->toXML(); } else { - $json .= "<$fieldName>" . Convert::raw2xml($fieldValue) . "\n"; + $xml .= "<$fieldName>" . Convert::raw2xml($fieldValue) . "\n"; } } @@ -66,7 +66,7 @@ class XMLDataFormatter extends DataFormatter { } else { $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); } - $json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n"; + $xml .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"" . $obj->$fieldName . "\">\n"; } foreach($obj->has_many() as $relName => $relClass) { @@ -74,14 +74,14 @@ class XMLDataFormatter extends DataFormatter { if($fields && !in_array($relName, $fields)) continue; if($this->customRelations && !in_array($relName, $this->customRelations)) continue; - $json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n"; + $xml .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n"; $items = $obj->$relName(); foreach($items as $item) { //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + $xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\">\n"; } - $json .= "\n"; + $xml .= "\n"; } foreach($obj->many_many() as $relName => $relClass) { @@ -89,19 +89,19 @@ class XMLDataFormatter extends DataFormatter { if($fields && !in_array($relName, $fields)) continue; if($this->customRelations && !in_array($relName, $this->customRelations)) continue; - $json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n"; + $xml .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n"; $items = $obj->$relName(); foreach($items as $item) { $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + $xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\">\n"; } - $json .= "\n"; + $xml .= "\n"; } } - $json .= ""; - - return $json; + $xml .= ""; + + return $xml; } /** diff --git a/core/Convert.php b/core/Convert.php index e80cceaaf..becd8a9d7 100755 --- a/core/Convert.php +++ b/core/Convert.php @@ -49,22 +49,33 @@ class Convert extends Object { } } - static function raw2xml($val) { + /** + * Ensure that text is properly escaped for XML. + * + * @param array|string $val String to escape, or array of strings + * @return array|string + */ + static function raw2xml($val) { if(is_array($val)) { foreach($val as $k => $v) $val[$k] = self::raw2xml($v); return $val; - } else { return str_replace(array('&', '<', '>', "\n"), array('&', '<', '>', '
'), $val); } } + + /** + * Ensure that text is properly escaped for Javascript. + * + * @param array|string $val String to escape, or array of strings + * @return array|string + */ static function raw2js($val) { if(is_array($val)) { foreach($val as $k => $v) $val[$k] = self::raw2js($v); return $val; - } else { - return str_replace(array("\\", '"',"\n","\r", "'"), array("\\\\", '\"','\n','\r', "\\'"), $val); + return str_replace(array("\\", '"', "\n", "\r", "'"), array("\\\\", '\"', '\n', '\r', "\\'"), $val); } } diff --git a/core/control/ContentNegotiator.php b/core/control/ContentNegotiator.php index 39b08d162..e3555f5c1 100755 --- a/core/control/ContentNegotiator.php +++ b/core/control/ContentNegotiator.php @@ -55,7 +55,7 @@ class ContentNegotiator { * @usedby Controller->handleRequest() */ static function process(HTTPResponse $response) { - if(self::$disabled) return; + if(!self::enabled_for($response)) return; $mimes = array( "xhtml" => "application/xhtml+xml", @@ -138,19 +138,46 @@ class ContentNegotiator { $response->addHeader("Vary", "Accept"); $content = $response->getBody(); + $hasXMLHeader = (substr($content,0,5) == '<' . '?xml' ); $content = ereg_replace("<\\?xml[^>]+\\?>\n?",'',$content); $content = str_replace(array('/>','xml:lang','application/xhtml+xml'),array('>','lang','text/html'), $content); - $content = ereg_replace(']+>', '', $content); + + // Only replace the doctype in templates with the xml header + if($hasXMLHeader) { + $content = ereg_replace(']+>', '', $content); + } $content = ereg_replace('setBody($content); } - protected static $disabled; - static function disable() { - self::$disabled = true; + protected static $enabled = false; + + /** + * Enable content negotiation for all templates, not just those with the xml header. + */ + static function enable() { + self::$enabled = true; } + + /** + * @deprecated in 2.3 + */ + static function disable() { + self::$enabled = false; + } + + + /** + * Returns true if negotation is enabled for the given response. + * By default, negotiation is only enabled for pages that have the xml header. + */ + static function enabled_for($response) { + if(self::$enabled) return true; + else return (substr($response->getBody(),0,5) == '<' . '?xml'); + } + } ?> diff --git a/core/i18n.php b/core/i18n.php index 81c4b26e0..377a068ac 100755 --- a/core/i18n.php +++ b/core/i18n.php @@ -811,12 +811,22 @@ class i18n extends Object { */ static function _t($entity, $string = "", $priority = 40, $context = "") { global $lang; + + // get current locale (either default or user preference) $locale = i18n::get_locale(); + + // parse $entity into its parts $entityParts = explode('.',$entity); $realEntity = array_pop($entityParts); $class = implode('.',$entityParts); - if(!isset($lang[$locale][$class])) i18n::include_by_class($class); + + // if language table isn't loaded for this locale, get it for each of the modules + if(!isset($lang[$locale])) i18n::include_by_locale($locale); + + // fallback to the passed $string if no translation is present $transEntity = isset($lang[$locale][$class][$realEntity]) ? $lang[$locale][$class][$realEntity] : $string; + + // entities can be stored in both array and literal values in the language tables return (is_array($transEntity) ? $transEntity[0] : $transEntity); } diff --git a/core/i18nTextCollector.php b/core/i18nTextCollector.php index 0134863a8..126a3bab7 100644 --- a/core/i18nTextCollector.php +++ b/core/i18nTextCollector.php @@ -63,15 +63,25 @@ class i18nTextCollector extends Object { * and write the resultant files in the lang folder of each module. * * @uses DataObject->collectI18nStatics() + * + * @param array $restrictToModules */ - public function run($restrictToModule = null) { + public function run($restrictToModules = null) { //Debug::message("Collecting text...", false); + $modules = array(); + // A master string tables array (one mst per module) $entitiesByModule = array(); //Search for and process existent modules, or use the passed one instead - $modules = (isset($restrictToModule)) ? array(basename($restrictToModule)) : scandir($this->basePath); + if($restrictToModules && count($restrictToModules)) { + foreach($restrictToModules as $restrictToModule) { + $modules[] = basename($restrictToModule); + } + } else { + $modules = scandir($this->basePath); + } foreach($modules as $module) { // Only search for calls in folder with a _config.php file (which means they are modules) @@ -83,9 +93,25 @@ class i18nTextCollector extends Object { if(!$isValidModuleFolder) continue; // we store the master string tables - $entitiesByModule[$module] = $this->processModule($module); + $processedEntities = $this->processModule($module); + if(isset($entitiesByModule[$module])) { + $entitiesByModule[$module] = array_merge_recursive($entitiesByModule[$module], $processedEntities); + } else { + $entitiesByModule[$module] = $processedEntities; + } + + // extract all entities for "foreign" modules (fourth argument) + foreach($entitiesByModule[$module] as $fullName => $spec) { + if(isset($spec[3]) && $spec[3] != $module) { + $othermodule = $spec[3]; + if(!isset($entitiesByModule[$othermodule])) $entitiesByModule[$othermodule] = array(); + unset($spec[3]); + $entitiesByModule[$othermodule][$fullName] = $spec; + unset($entitiesByModule[$module][$fullName]); + } + } } - + // Write the generated master string tables $this->writeMasterStringFile($entitiesByModule); diff --git a/core/i18nTextCollectorTask.php b/core/i18nTextCollectorTask.php index d7d75ad66..dbb06e129 100644 --- a/core/i18nTextCollectorTask.php +++ b/core/i18nTextCollectorTask.php @@ -26,7 +26,8 @@ class i18nTextCollectorTask extends BuildTask { */ public function run($request) { $c = new i18nTextCollector(); - return $c->run($request->getVar('module')); + $restrictModules = ($request->getVar('module')) ? explode(',', $request->getVar('module')) : null; + return $c->run($restrictModules); } } ?> \ No newline at end of file diff --git a/core/model/DataObject.php b/core/model/DataObject.php index f82ee1c06..6ecd09be1 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -711,7 +711,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP // Update the changed array with references to changed obj-fields foreach($this->record as $k => $v) { - if(is_object($v) && $v->isChanged()) { + if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) { $this->changed[$k] = true; } } @@ -1302,7 +1302,7 @@ class DataObject extends ViewableData implements DataObjectInterface,i18nEntityP if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$db has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a property name, and the map value should be the property type.", E_USER_ERROR); } - $items = isset($items) ? array_merge($newItems, (array)$items) : $newItems; + $items = isset($items) ? array_merge((array)$items, $newItems) : $newItems; } } diff --git a/core/model/DatabaseAdmin.php b/core/model/DatabaseAdmin.php index 3f1e5d4c3..9ec29e76d 100644 --- a/core/model/DatabaseAdmin.php +++ b/core/model/DatabaseAdmin.php @@ -77,8 +77,8 @@ class DatabaseAdmin extends Controller { // Get all our classes ManifestBuilder::create_manifest_file(); require(MANIFEST_FILE); - - $this->doBuild(isset($_REQUEST['quiet']) || isset($_REQUEST['from_installer'])); + + $this->doBuild(isset($_REQUEST['quiet']) || isset($_REQUEST['from_installer']), !isset($_REQUEST['dont_populate'])); } /** diff --git a/core/model/SQLMap.php b/core/model/SQLMap.php index b1b66460f..809a74ca8 100755 --- a/core/model/SQLMap.php +++ b/core/model/SQLMap.php @@ -11,17 +11,22 @@ class SQLMap extends Object implements IteratorAggregate { * @var SQLQuery */ protected $query; + protected $keyField, $titleField; /** * Construct a SQLMap. * @param SQLQuery $query The query to generate this map. THis isn't executed until it's needed. */ - public function __construct(SQLQuery $query) { + public function __construct(SQLQuery $query, $keyField = "ID", $titleField = "Title") { if(!$query) { user_error('SQLMap constructed with null query.', E_USER_ERROR); } $this->query = $query; + $this->keyField = $keyField; + $this->titleField = $titleField; + + parent::__construct(); } /** @@ -44,7 +49,7 @@ class SQLMap extends Object implements IteratorAggregate { public function getIterator() { $this->genItems(); - return new SQLMap_Iterator($this->items->getIterator()); + return new SQLMap_Iterator($this->items->getIterator(), $this->keyField, $this->titleField); } /** @@ -80,9 +85,12 @@ class SQLMap extends Object implements IteratorAggregate { class SQLMap_Iterator extends Object implements Iterator { protected $items; + protected $keyField, $titleField; - function __construct(Iterator $items) { + function __construct(Iterator $items, $keyField, $titleField) { $this->items = $items; + $this->keyField = $keyField; + $this->titleField = $titleField; } @@ -90,20 +98,20 @@ class SQLMap_Iterator extends Object implements Iterator { * Iterator functions - necessary for foreach to work */ public function rewind() { - return $this->items->rewind() ? $this->items->rewind()->Title : null; + return $this->items->rewind() ? $this->items->rewind()->{$this->titleField} : null; } public function current() { - return $this->items->current()->Title; + return $this->items->current()->{$this->titleField}; } public function key() { - return $this->items->current()->ID; + return $this->items->current()->{$this->keyField}; } public function next() { $next = $this->items->next(); - return isset($next->Title) ? $next->Title : null; + return isset($next->{$this->titleField}) ? $next->{$this->titleField} : null; } public function valid() { diff --git a/core/model/SQLQuery.php b/core/model/SQLQuery.php index 7f5e4c4d8..20185c05b 100755 --- a/core/model/SQLQuery.php +++ b/core/model/SQLQuery.php @@ -415,7 +415,16 @@ class SQLQuery extends Object { * * TODO Respect HAVING and GROUPBY, which can affect the result-count */ - function unlimitedRowCount( $column = "*" ) { + function unlimitedRowCount( $column = null) { + // Choose a default column + if($column == null) { + if($this->groupby) { + $column = 'DISTINCT ' . implode(", ", $this->groupby); + } else { + $column = '*'; + } + } + $clone = clone $this; $clone->select = array("count($column)"); $clone->limit = null; diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 29ca7ab4b..1c53616e2 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -7,7 +7,7 @@ * In addition, it contains a number of static methods for querying the site tree. * @package cms */ -class SiteTree extends DataObject implements PermissionProvider { +class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider { /** * Indicates what kind of children this page type can have. @@ -1485,7 +1485,6 @@ class SiteTree extends DataObject implements PermissionProvider { $classes = ClassInfo::getValidSubClasses('SiteTree'); array_shift($classes); - $currentAddAction = null; $currentClass = null; $result = array(); @@ -1493,22 +1492,45 @@ class SiteTree extends DataObject implements PermissionProvider { $instance = singleton($class); if((($instance instanceof HiddenClass) || !$instance->canCreate()) && ($class != $this->class)) continue; - $addAction = $instance->i18n_singular_name(); + $pageTypeName = $instance->i18n_singular_name(); if($class == $this->class) { $currentClass = $class; - $currentAddAction = $addAction; + $result[$class] = $pageTypeName; } else { - $result[$class] = ($class == $this->class) - ? _t('SiteTree.CURRENTLY', 'Currently').' '.$addAction - : _t('SiteTree.CHANGETO', 'Change to').' '.$addAction; + $translation = _t( + 'SiteTree.CHANGETO', + 'Change to "%s"', + PR_MEDIUM, + "Pagetype selection dropdown with class names" + ); + + // @todo legacy fix to avoid empty classname dropdowns when translation doesn't include %s + if(strpos($translation, '%s') !== FALSE) { + $result[$class] = sprintf( + $translation, + $pageTypeName + ); + } else { + $result[$class] = "{$translation} \"{$pageTypeName}\""; + } + } + + // if we're in translation mode, the link between the translated pagetype + // title and the actual classname might not be obvious, so we add it in parantheses + // Example: class "RedirectorPage" has the title "Weiterleitung" in German, + // so it shows up as "Weiterleitung (RedirectorPage)" + if(i18n::get_locale() != 'en_US') { + $result[$class] = $result[$class] . " ({$class})"; } } // sort alphabetically, and put current on top asort($result); + $currentPageTypeName = $result[$currentClass]; + unset($result[$currentClass]); $result = array_reverse($result); - $result[$currentClass] = $currentAddAction.' ('._t('SiteTree.CURRENT','current').')'; + $result[$currentClass] = $currentPageTypeName; $result = array_reverse($result); return $result; @@ -1739,6 +1761,19 @@ class SiteTree extends DataObject implements PermissionProvider { ) ); } + + /** + * Overloaded to also provide entities for 'Page' class which is usually + * located in custom code, hence textcollector picks it up for the wrong folder. + */ + function provideI18nEntities() { + $entities = parent::provideI18nEntities(); + + if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = 'sapphire'; + if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = 'sapphire'; + + return $entities; + } } ?> diff --git a/core/model/fieldtypes/Date.php b/core/model/fieldtypes/Date.php index f0b718847..a81d643f2 100644 --- a/core/model/fieldtypes/Date.php +++ b/core/model/fieldtypes/Date.php @@ -16,7 +16,7 @@ class Date extends DBField { if(ereg('^([0-9]+)/([0-9]+)/([0-9]+)$', $value, $parts)) $value = "$parts[2]/$parts[1]/$parts[3]"; - if($value) $this->value = date('Y-m-d', strtotime($value)); + if($value && is_string($value)) $this->value = date('Y-m-d', strtotime($value)); else $value = null; } @@ -95,10 +95,27 @@ class Date extends DBField { */ function Ago() { if($this->value) { - if(time() < strtotime($this->value)) $agoWord = _t("Date.AWAY", " away"); - else $agoWord = _t("Date.AGO", " ago"); - - return $this->TimeDiff() . ' ' . $agoWord; + if(time() > strtotime($this->value)) { + return sprintf( + _t( + 'Date.TIMEDIFFAGO', + "%s ago", + PR_MEDIUM, + 'Natural language time difference, e.g. 2 hours ago' + ), + $this->TimeDiff() + ); + } else { + return sprintf( + _t( + 'Date.TIMEDIFFAWAY', + "%s away", + PR_MEDIUM, + 'Natural language time difference, e.g. 2 hours away' + ), + $this->TimeDiff() + ); + } } } @@ -109,27 +126,27 @@ class Date extends DBField { if($ago < 60) { $span = $ago; - return ($span != 1) ? "{$span}"._t("Date.SECS", " secs") : "{$span}"._t("Date.SEC", " sec"); + return ($span != 1) ? "{$span} "._t("Date.SECS", " secs") : "{$span} "._t("Date.SEC", " sec"); } if($ago < 3600) { $span = round($ago/60); - return ($span != 1) ? "{$span}"._t("Date.MINS", " mins") : "{$span}"._t("Date.MIN", " min"); + return ($span != 1) ? "{$span} "._t("Date.MINS", " mins") : "{$span} "._t("Date.MIN", " min"); } if($ago < 86400) { $span = round($ago/3600); - return ($span != 1) ? "{$span}"._t("Date.HOURS", " hours") : "{$span}"._t("Date.HOUR", " hour"); + return ($span != 1) ? "{$span} "._t("Date.HOURS", " hours") : "{$span} "._t("Date.HOUR", " hour"); } if($ago < 86400*30) { $span = round($ago/86400); - return ($span != 1) ? "{$span}"._t("Date.DAYS", " days") : "{$span}"._t("Date.DAY", " day"); + return ($span != 1) ? "{$span} "._t("Date.DAYS", " days") : "{$span} "._t("Date.DAY", " day"); } if($ago < 86400*365) { $span = round($ago/86400/30); - return ($span != 1) ? "{$span}"._t("Date.MONTHS", " months") : "{$span}"._t("Date.MONTH", " month"); + return ($span != 1) ? "{$span} "._t("Date.MONTHS", " months") : "{$span} "._t("Date.MONTH", " month"); } if($ago > 86400*365) { $span = round($ago/86400/365); - return ($span != 1) ? "{$span}"._t("Date.YEARS", " years") : "{$span}"._t("Date.YEAR", " year"); + return ($span != 1) ? "{$span} "._t("Date.YEARS", " years") : "{$span} "._t("Date.YEAR", " year"); } } } diff --git a/core/model/fieldtypes/ForeignKey.php b/core/model/fieldtypes/ForeignKey.php index 172b8b6d1..d5beaf8b8 100644 --- a/core/model/fieldtypes/ForeignKey.php +++ b/core/model/fieldtypes/ForeignKey.php @@ -43,9 +43,8 @@ class ForeignKey extends Int { $field = new FileField($relationName, $title, $this->value); } } else { - $objs = DataObject::get($hasOneClass); $titleField = (singleton($hasOneClass)->hasField('Title')) ? "Title" : "Name"; - $map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false; + $map = new SQLMap(singleton($hasOneClass)->extendedSQL(), "ID", $titleField); $field = new DropdownField($this->name, $title, $map, null, null, ' '); } diff --git a/core/model/fieldtypes/PrimaryKey.php b/core/model/fieldtypes/PrimaryKey.php index 1f3c181bd..379fcebe4 100644 --- a/core/model/fieldtypes/PrimaryKey.php +++ b/core/model/fieldtypes/PrimaryKey.php @@ -22,12 +22,8 @@ class PrimaryKey extends Int { } public function scaffoldFormField($title = null, $params = null) { - $objs = DataObject::get($this->object->class); - - $titleField = (singleton($this->object->class)->hasField('Title')) ? "Title" : "Name"; - - $map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false; - + $titleField = ($this->object->hasField('Title')) ? "Title" : "Name"; + $map = new SQLMap($this->object->extendedSQL(), "ID", $titleField); return new DropdownField($this->name, $title, $map, null, null, ' '); } } diff --git a/core/model/fieldtypes/Text.php b/core/model/fieldtypes/Text.php index ce17786b3..5f06ca58c 100644 --- a/core/model/fieldtypes/Text.php +++ b/core/model/fieldtypes/Text.php @@ -19,16 +19,24 @@ class Text extends DBField { return ($this->value || $this->value == '0'); } - //useed for search results show only limited contents - function LimitWordCount($numWords = 26) { - $this->value = Convert::xml2raw($this->value); - $ret = explode(" ", $this->value, $numWords); + /** + * Limit this field's content by a number of words. + * CAUTION: This is not XML safe. Please use + * {@link LimitWordCountXML()} instead. + * + * @param int $numWords Number of words to limit by + * @param string $add Ellipsis to add to the end of truncated string + * @return string + */ + function LimitWordCount($numWords = 26, $add = '...') { + $this->value = trim(Convert::xml2raw($this->value)); + $ret = explode(' ', $this->value, $numWords + 1); - if( Count($ret) < $numWords-1 ){ - $ret=$this->value; - }else{ + if(count($ret) <= $numWords - 1) { + $ret = $this->value; + } else { array_pop($ret); - $ret=implode(" ", $ret)."..."; + $ret = implode(' ', $ret) . $add; } return $ret; @@ -45,11 +53,23 @@ class Text extends DBField { return HTTP::absoluteURLs($this->value); } + /** + * Limit this field's content by a number of characters. + * CAUTION: Does not take into account HTML tags, so it + * has the potential to return malformed HTML. + * + * @param int $limit Number of characters to limit by + * @param string $add Ellipsis to add to the end of truncated string + * @return string + */ function LimitCharacters($limit = 20, $add = "...") { $value = trim($this->value); return (strlen($value) > $limit) ? substr($value, 0, $limit) . $add : $value; } + /** + * @deprecated. Please use {@link LimitWordCount()} + */ function LimitWordCountPlainText($numWords = 26) { $ret = $this->LimitWordCount( $numWords ); // Use LimitWordCountXML() instead! @@ -57,11 +77,18 @@ class Text extends DBField { return $ret; } - function LimitWordCountXML( $numWords = 26 ) { - $ret = $this->LimitWordCount( $numWords ); - $ret = Convert::raw2xml($ret); - - return $ret; + /** + * Limit the number of words of the current field's + * content. This is XML safe, so characters like & + * are converted to & + * + * @param int $numWords Number of words to limit by + * @param string $add Ellipsis to add to the end of truncated string + * @return string + */ + function LimitWordCountXML($numWords = 26, $add = '...') { + $ret = $this->LimitWordCount($numWords, $add); + return Convert::raw2xml($ret); } /** @@ -221,12 +248,19 @@ class Text extends DBField { } } + /** + * Perform context searching to give some context to searches, optionally + * highlighting the search term. + * + * @param int $characters Number of characters in the summary + * @param boolean $string Supplied string ("keywords") + * @param boolean $striphtml Strip HTML? + * @param boolean $highlight Add a highlight element around search query? + * @return string + */ function ContextSummary($characters = 500, $string = false, $striphtml = true, $highlight = true) { - if(!$string) { - // If no string is supplied, use the string from a SearchForm - $string = $_REQUEST['Search']; - } - + if(!$string) $string = $_REQUEST['Search']; // Use the default "Search" request variable (from SearchForm) + // Remove HTML tags so we don't have to deal with matching tags $text = $striphtml ? $this->NoHTML() : $this->value; @@ -236,7 +270,6 @@ class Text extends DBField { // We want to search string to be in the middle of our block to give it some context $position = max(0, $position - ($characters / 2)); - if($position > 0) { // We don't want to start mid-word $position = max((int) strrpos(substr($text, 0, $position), ' '), (int) strrpos(substr($text, 0, $position), "\n")); @@ -244,13 +277,20 @@ class Text extends DBField { $summary = substr($text, $position, $characters); + $stringPieces = explode(' ', $string); + if($highlight) { // Add a span around all occurences of the search term $summary = str_ireplace($string, "$string", $summary); + + // Add a span around all key words from the search term as well + if($stringPieces) { + foreach($stringPieces as $stringPiece) { + $summary = str_ireplace($stringPiece, "$stringPiece", $summary); + } + } } - // trim it, because if we counted back and found a space then there will be an extra - // space at the front return trim($summary); } diff --git a/css/Form.css b/css/Form.css index 5b3a9e315..ec5f8e307 100644 --- a/css/Form.css +++ b/css/Form.css @@ -105,6 +105,10 @@ form button.minorAction { /** * Messages */ +form .message.notice { + background-color: #FCFFDF; + border-color: #FF9300; +} form .message { margin: 1em 0; padding: 0.5em; diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index ab274f4a6..4dce4b7a3 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -16,6 +16,38 @@ class DevelopmentAdmin extends Controller { '$Action//$Action/$ID' => 'handleAction', ); + + function init() { + parent::init(); + + // check for valid url mapping + // lacking this information can cause really nasty bugs, + // e.g. when running Director::test() from a FunctionalTest instance + global $_FILE_TO_URL_MAPPING; + if(Director::is_cli()) { + if(isset($_FILE_TO_URL_MAPPING)) { + $fullPath = $testPath = $_SERVER['SCRIPT_FILENAME']; + while($testPath && $testPath != "/") { + $matched = false; + if(isset($_FILE_TO_URL_MAPPING[$testPath])) { + $matched = true; + break; + } + $testPath = dirname($testPath); + } + if(!$matched) { + echo 'Warning: You probably want to define '. + 'an entry in $_FILE_TO_URL_MAPPING that covers "' . Director::baseFolder() . '"' . "\n"; + } + } + else { + echo 'Warning: You probably want to define $_FILE_TO_URL_MAPPING in '. + 'your _ss_environment.php as instructed on the "sake" page of the doc.silverstripe.com wiki' . "\n"; + } + } + + } + function index() { $actions = array( "build" => "Build/rebuild this environment (formerly db/build). Call this whenever you have updated your project sources", diff --git a/dev/TaskRunner.php b/dev/TaskRunner.php index 198f7b840..83df1f581 100644 --- a/dev/TaskRunner.php +++ b/dev/TaskRunner.php @@ -19,11 +19,12 @@ class TaskRunner extends Controller { $renderer->writeHeader(); $renderer->writeInfo("Sapphire Development Tools: Tasks", Director::absoluteBaseURL()); $base = Director::baseURL(); - + if(strpos($base,-1) != '/') $base .= '/'; + echo "