diff --git a/.travis.yml b/.travis.yml index a6b5e52e5..ee5febf23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,6 @@ matrix: env: DB=MYSQL CORE_RELEASE=master - php: 5.4 env: DB=MYSQL CORE_RELEASE=master BEHAT_TEST=1 - allow_failures: - php: 5.6 env: DB=MYSQL CORE_RELEASE=master diff --git a/admin/_config.php b/admin/_config.php index 2e760267d..20ba01586 100644 --- a/admin/_config.php +++ b/admin/_config.php @@ -14,7 +14,7 @@ HtmlEditorConfig::get('cms')->setOptions(array( . "|class],-strong/-b[class],-em/-i[class],-strike[class],-u[class],#p[id|dir|class|align|style],-ol[class]," . "-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align|data*]," . "-sub[class],-sup[class],-blockquote[dir|class],-cite[dir|class|id|title]," - . "-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|dir|id|style]," + . "-table[cellspacing|cellpadding|width|height|class|align|summary|dir|id|style]," . "-tr[id|dir|class|rowspan|width|height|align|valign|bgcolor|background|bordercolor|style]," . "tbody[id|class|style],thead[id|class|style],tfoot[id|class|style]," . "#td[id|dir|class|colspan|rowspan|width|height|align|valign|scope|style]," diff --git a/control/Controller.php b/control/Controller.php index 481c8bd4e..92c85bac8 100644 --- a/control/Controller.php +++ b/control/Controller.php @@ -429,7 +429,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider { if(isset(self::$controller_stack[1])) { $this->session = self::$controller_stack[1]->getSession(); } else { - $this->session = new Session(null); + $this->session = Injector::inst()->create('Session', array()); } } } diff --git a/control/Director.php b/control/Director.php index 712a3569f..f2e97d7c7 100644 --- a/control/Director.php +++ b/control/Director.php @@ -135,12 +135,13 @@ class Director implements TemplateGlobalProvider { $req->addHeader($header, $value); } + // Initiate an empty session - doesn't initialize an actual PHP session until saved (see below) + $session = Injector::inst()->create('Session', isset($_SESSION) ? $_SESSION : array()); + // Only resume a session if its not started already, and a session identifier exists if(!isset($_SESSION) && Session::request_contains_session_id()) { - Session::start(); + $session->inst_start(); } - // Initiate an empty session - doesn't initialize an actual PHP session until saved (see belwo) - $session = new Session(isset($_SESSION) ? $_SESSION : null); $output = Injector::inst()->get('RequestProcessor')->preRequest($req, $session, $model); @@ -151,7 +152,7 @@ class Director implements TemplateGlobalProvider { $result = Director::handleRequest($req, $session, $model); - // Save session data (and start/resume it if required) + // Save session data. Note that inst_save() will start/resume the session if required. $session->inst_save(); // Return code for a redirection request @@ -229,7 +230,7 @@ class Director implements TemplateGlobalProvider { if(!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET"; - if(!$session) $session = new Session(null); + if(!$session) $session = Injector::inst()->create('Session', array()); // Back up the current values of the superglobals $existingRequestVars = isset($_REQUEST) ? $_REQUEST : array(); @@ -982,29 +983,21 @@ class Director implements TemplateGlobalProvider { /* * This function will return true if the site is in a live environment. * For information about environment types, see {@link Director::set_environment_type()}. - * - * @param $skipDatabase Skips database checks for current login permissions if set to TRUE, - * which is useful for checks happening before the database is functional. */ - public static function isLive($skipDatabase = false) { - return !(Director::isDev($skipDatabase) || Director::isTest($skipDatabase)); + public static function isLive() { + return !(Director::isDev() || Director::isTest()); } /** * This function will return true if the site is in a development environment. * For information about environment types, see {@link Director::set_environment_type()}. - * - * @param $skipDatabase Skips database checks for current login permissions if set to TRUE, - * which is useful for checks happening before the database is functional. */ - public static function isDev($skipDatabase = false) { - // This variable is used to supress repetitions of the isDev security message below. - static $firstTimeCheckingGetVar = true; - - $result = false; - - if(isset($_SESSION['isDev']) && $_SESSION['isDev']) $result = true; - if(Config::inst()->get('Director', 'environment_type') == 'dev') $result = true; + public static function isDev() { + // Check session + if($env = self::session_environment()) return $env === 'dev'; + + // Check config + if(Config::inst()->get('Director', 'environment_type') === 'dev') return true; // Check if we are running on one of the test servers $devServers = (array)Config::inst()->get('Director', 'dev_servers'); @@ -1012,54 +1005,22 @@ class Director implements TemplateGlobalProvider { return true; } - // Use ?isDev=1 to get development access on the live server - if(!$skipDatabase && !$result && isset($_GET['isDev'])) { - if(Security::database_is_ready()) { - if($firstTimeCheckingGetVar && !Permission::check('ADMIN')){ - BasicAuth::requireLogin("SilverStripe developer access. Use your CMS login", "ADMIN"); - } - $_SESSION['isDev'] = $_GET['isDev']; - $firstTimeCheckingGetVar = false; - $result = $_GET['isDev']; - } else { - if($firstTimeCheckingGetVar && DB::connection_attempted()) { - echo "
Sorry, you can't use ?isDev=1 until your - Member and Group tables database are available. Perhaps your database - connection is failing?
"; - $firstTimeCheckingGetVar = false; - } - } - } - - return $result; + return false; } /** * This function will return true if the site is in a test environment. * For information about environment types, see {@link Director::set_environment_type()}. - * - * @param $skipDatabase Skips database checks for current login permissions if set to TRUE, - * which is useful for checks happening before the database is functional. */ - public static function isTest($skipDatabase = false) { - // Use ?isTest=1 to get test access on the live server, or explicitly set your environment - if(!$skipDatabase && isset($_GET['isTest'])) { - if(Security::database_is_ready()) { - BasicAuth::requireLogin("SilverStripe developer access. Use your CMS login", "ADMIN"); - $_SESSION['isTest'] = $_GET['isTest']; - } else { - return true; - } - } + public static function isTest() { + // In case of isDev and isTest both being set, dev has higher priority + if(self::isDev()) return false; - if(self::isDev($skipDatabase)) { - return false; - } + // Check saved session + if($env = self::session_environment()) return $env === 'test'; - if(Config::inst()->get('Director', 'environment_type')) { - return Config::inst()->get('Director', 'environment_type') == 'test'; - } + // Check config + if(Config::inst()->get('Director', 'environment_type') === 'test') return true; // Check if we are running on one of the test servers $testServers = (array)Config::inst()->get('Director', 'test_servers'); @@ -1069,6 +1030,36 @@ class Director implements TemplateGlobalProvider { return false; } + + /** + * Check or update any temporary environment specified in the session + * + * @return string 'test', 'dev', or null + */ + protected static function session_environment() { + // Set session from querystring + if(isset($_GET['isDev'])) { + if(isset($_SESSION)) { + unset($_SESSION['isTest']); // In case we are changing from test mode + $_SESSION['isDev'] = $_GET['isDev']; + } + return 'dev'; + } elseif(isset($_GET['isTest'])) { + if(isset($_SESSION)) { + unset($_SESSION['isDev']); // In case we are changing from dev mode + $_SESSION['isTest'] = $_GET['isTest']; + } + return 'test'; + } + // Check session + if(isset($_SESSION['isDev']) && $_SESSION['isDev']) { + return 'dev'; + } elseif(isset($_SESSION['isTest']) && $_SESSION['isTest']) { + return 'test'; + } else { + return null; + } + } /** * @return array Returns an array of strings of the method names of methods on the call that should be exposed diff --git a/control/Session.php b/control/Session.php index a384de0e1..815afd08e 100644 --- a/control/Session.php +++ b/control/Session.php @@ -139,20 +139,19 @@ class Session { /** * Start PHP session, then create a new Session object with the given start data. * - * @param $data Can be an array of data (such as $_SESSION) or another Session object to clone. + * @param $data array|Session Can be an array of data (such as $_SESSION) or another Session object to clone. */ public function __construct($data) { if($data instanceof Session) $data = $data->inst_getAll(); $this->data = $data; - + if (isset($this->data['HTTP_USER_AGENT'])) { if ($this->data['HTTP_USER_AGENT'] != $this->userAgent()) { // Funny business detected! $this->inst_clearAll(); - - Session::destroy(); - Session::start(); + $this->inst_destroy(); + $this->inst_start(); } } } @@ -347,11 +346,78 @@ class Session { if(Controller::has_curr()) { return Controller::curr()->getSession(); } else { - if(!self::$default_session) self::$default_session = new Session(isset($_SESSION) ? $_SESSION : array()); + if(!self::$default_session) { + self::$default_session = Injector::inst()->create('Session', isset($_SESSION) ? $_SESSION : array()); + } + return self::$default_session; } } + public function inst_start($sid = null) { + $path = Config::inst()->get('Session', 'cookie_path'); + if(!$path) $path = Director::baseURL(); + $domain = Config::inst()->get('Session', 'cookie_domain'); + $secure = Director::is_https() && Config::inst()->get('Session', 'cookie_secure'); + $session_path = Config::inst()->get('Session', 'session_store_path'); + $timeout = Config::inst()->get('Session', 'timeout'); + + if(!session_id() && !headers_sent()) { + if($domain) { + session_set_cookie_params($timeout, $path, $domain, $secure, true); + } else { + session_set_cookie_params($timeout, $path, null, $secure, true); + } + + // Allow storing the session in a non standard location + if($session_path) session_save_path($session_path); + + // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a + // seperate (less secure) session for non-HTTPS requests + if($secure) session_name('SECSESSID'); + + // @ is to supress win32 warnings/notices when session wasn't cleaned up properly + // There's nothing we can do about this, because it's an operating system function! + if($sid) session_id($sid); + @session_start(); + + $this->data = isset($_SESSION) ? $_SESSION : array(); + } + + // Modify the timeout behaviour so it's the *inactive* time before the session expires. + // By default it's the total session lifetime + if($timeout && !headers_sent()) { + Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain + : null, $secure, true); + } + } + + public function inst_destroy($removeCookie = true) { + if(session_id()) { + if($removeCookie) { + $path = Config::inst()->get('Session', 'cookie_path'); + if(!$path) $path = Director::baseURL(); + $domain = Config::inst()->get('Session', 'cookie_domain'); + $secure = Config::inst()->get('Session', 'cookie_secure'); + + if($domain) { + Cookie::set(session_name(), '', null, $path, $domain, $secure, true); + } else { + Cookie::set(session_name(), '', null, $path, null, $secure, true); + } + + unset($_COOKIE[session_name()]); + } + + session_destroy(); + + // Clean up the superglobal - session_destroy does not do it. + // http://nz1.php.net/manual/en/function.session-destroy.php + unset($_SESSION); + $this->data = array(); + } + } + public function inst_set($name, $val) { // Quicker execution path for "."-free names if(strpos($name,'.') === false) { @@ -472,7 +538,11 @@ class Session { public function inst_save() { if($this->changedData) { $this->inst_finalize(); - if(!isset($_SESSION)) Session::start(); + + if(!isset($_SESSION)) { + $this->inst_start(); + } + $this->recursivelyApply($this->changedData, $_SESSION); } } @@ -529,41 +599,7 @@ class Session { * @param string $sid Start the session with a specific ID */ public static function start($sid = null) { - $path = Config::inst()->get('Session', 'cookie_path'); - if(!$path) $path = Director::baseURL(); - $domain = Config::inst()->get('Session', 'cookie_domain'); - $secure = Director::is_https() && Config::inst()->get('Session', 'cookie_secure'); - $session_path = Config::inst()->get('Session', 'session_store_path'); - $timeout = Config::inst()->get('Session', 'timeout'); - - if(!session_id() && !headers_sent()) { - if($domain) { - session_set_cookie_params($timeout, $path, $domain, - $secure /* secure */, true /* httponly */); - } else { - session_set_cookie_params($timeout, $path, null, - $secure /* secure */, true /* httponly */); - } - - // Allow storing the session in a non standard location - if($session_path) session_save_path($session_path); - - // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a - // seperate (less secure) session for non-HTTPS requests - if($secure) session_name('SECSESSID'); - - // @ is to supress win32 warnings/notices when session wasn't cleaned up properly - // There's nothing we can do about this, because it's an operating system function! - if($sid) session_id($sid); - @session_start(); - } - - // Modify the timeout behaviour so it's the *inactive* time before the session expires. - // By default it's the total session lifetime - if($timeout && !headers_sent()) { - Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain - : null, $secure, true); - } + self::current_session()->inst_start($sid); } /** @@ -572,29 +608,7 @@ class Session { * @param bool $removeCookie If set to TRUE, removes the user's cookie, FALSE does not remove */ public static function destroy($removeCookie = true) { - if(session_id()) { - if($removeCookie) { - $path = Config::inst()->get('Session', 'cookie_path'); - if(!$path) $path = Director::baseURL(); - $domain = Config::inst()->get('Session', 'cookie_domain'); - $secure = Config::inst()->get('Session', 'cookie_secure'); - - if($domain) { - Cookie::set(session_name(), '', null, $path, $domain, $secure, true); - } - else { - Cookie::set(session_name(), '', null, $path, null, $secure, true); - } - - unset($_COOKIE[session_name()]); - } - - session_destroy(); - - // Clean up the superglobal - session_destroy does not do it. - // http://nz1.php.net/manual/en/function.session-destroy.php - unset($_SESSION); - } + self::current_session()->inst_destroy($removeCookie); } /** diff --git a/core/Convert.php b/core/Convert.php index 7c628d1e1..8007c3e38 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -116,7 +116,12 @@ class Convert { 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( + // Intercepts some characters such as <, >, and & which can interfere + array("\\", '"', "\n", "\r", "'", "<", ">", "&"), + array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"), + $val + ); } } diff --git a/core/startup/ParameterConfirmationToken.php b/core/startup/ParameterConfirmationToken.php index 495bd503a..35eb88a70 100644 --- a/core/startup/ParameterConfirmationToken.php +++ b/core/startup/ParameterConfirmationToken.php @@ -54,14 +54,49 @@ class ParameterConfirmationToken { // If a token was provided, but isn't valid, ignore it if ($this->token && (!$this->checkToken($this->token))) $this->token = null; } + + /** + * Get the name of this token + * + * @return string + */ + public function getName() { + return $this->parameterName; + } + /** + * Is the parameter requested? + * + * @return bool + */ public function parameterProvided() { return $this->parameter !== null; } + /** + * Is the necessary token provided for this parameter? + * + * @return bool + */ public function tokenProvided() { return $this->token !== null; } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + public function reloadRequired() { + return $this->parameterProvided() && !$this->tokenProvided(); + } + + /** + * Suppress the current parameter by unsetting it from $_GET + */ + public function suppress() { + unset($_GET[$this->parameterName]); + } public function params() { return array( @@ -139,4 +174,24 @@ You are being redirected. If you are not redirected soon, cl else header('location: '.$location, true, 302); die; } + + /** + * Given a list of token names, suppress all tokens that have not been validated, and + * return the non-validated token with the highest priority + * + * @param type $keys List of token keys in ascending priority (low to high) + * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority + */ + public static function prepare_tokens($keys) { + $target = null; + foreach($keys as $key) { + $token = new ParameterConfirmationToken($key); + // Validate this token + if($token->reloadRequired()) { + $token->suppress(); + $target = $token; + } + } + return $target; + } } diff --git a/dev/CliDebugView.php b/dev/CliDebugView.php index 9c39c486d..86ba02b19 100644 --- a/dev/CliDebugView.php +++ b/dev/CliDebugView.php @@ -41,7 +41,7 @@ class CliDebugView extends DebugView { foreach($lines as $offset => $line) { echo ($offset == $errline) ? "* " : " "; echo str_pad("$offset:",5); - echo wordwrap($line, 100, "\n "); + echo wordwrap($line, self::config()->columns, "\n "); } echo "\n"; } @@ -61,11 +61,25 @@ class CliDebugView extends DebugView { * @param string $title */ public function writeInfo($title, $subtitle, $description=false) { - echo wordwrap(strtoupper($title),100) . "\n"; - echo wordwrap($subtitle,100) . "\n"; - echo str_repeat('-',min(100,max(strlen($title),strlen($subtitle)))) . "\n"; - echo wordwrap($description,100) . "\n\n"; + echo wordwrap(strtoupper($title),self::config()->columns) . "\n"; + echo wordwrap($subtitle,self::config()->columns) . "\n"; + echo str_repeat('-',min(self::config()->columns,max(strlen($title),strlen($subtitle)))) . "\n"; + echo wordwrap($description,self::config()->columns) . "\n\n"; } + public function writeVariable($val, $caller) { + echo PHP_EOL; + echo SS_Cli::text(str_repeat('=', self::config()->columns), 'green'); + echo PHP_EOL; + echo SS_Cli::text($this->formatCaller($caller), 'blue', null, true); + echo PHP_EOL.PHP_EOL; + if (is_string($val)) { + print_r(wordwrap($val, self::config()->columns)); + } else { + print_r($val); + } + echo PHP_EOL; + echo SS_Cli::text(str_repeat('=', self::config()->columns), 'green'); + echo PHP_EOL; + } } - diff --git a/dev/Debug.php b/dev/Debug.php index f54f1b129..f854fea9b 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -71,6 +71,11 @@ class Debug { } + /** + * Returns the caller for a specific method + * + * @return array + */ public static function caller() { $bt = debug_backtrace(); $caller = $bt[2]; @@ -102,12 +107,7 @@ class Debug { * @param mixed $val */ public static function dump($val) { - echo '';
- $caller = Debug::caller();
- echo "" . basename($caller['file']) . ":$caller[line] - \n";
- if (is_string($val)) print_r(wordwrap($val, 100));
- else print_r($val);
- echo '
';
+ self::create_debug_view()->writeVariable($val, self::caller());
}
/**
@@ -296,10 +296,11 @@ class Debug {
);
if(Director::isDev() || Director::is_cli()) {
- return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
+ self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
} else {
- return self::friendlyError();
+ self::friendlyError();
}
+ return false;
}
/**
@@ -365,13 +366,17 @@ class Debug {
}
return false;
}
-
+
/**
* Create an instance of an appropriate DebugView object.
+ *
+ * @return DebugView
*/
public static function create_debug_view() {
- if(Director::is_cli() || Director::is_ajax()) return new CliDebugView();
- else return new DebugView();
+ $service = Director::is_cli() || Director::is_ajax()
+ ? 'CliDebugView'
+ : 'DebugView';
+ return Injector::inst()->get($service);
}
/**
diff --git a/dev/DebugView.php b/dev/DebugView.php
index 5d0d24a3a..ea9393e26 100644
--- a/dev/DebugView.php
+++ b/dev/DebugView.php
@@ -12,6 +12,14 @@
* @subpackage dev
*/
class DebugView extends Object {
+
+ /**
+ * Column size to wrap long strings to
+ *
+ * @var int
+ * @config
+ */
+ private static $columns = 100;
protected static $error_types = array(
E_USER_ERROR => array(
@@ -189,5 +197,32 @@ class DebugView extends Object {
public function writeParagraph($text) {
echo '' . $text . '
'; } + + /** + * Formats the caller of a method + * + * @param array $caller + * @return string + */ + protected function formatCaller($caller) { + $return = basename($caller['file']) . ":" . $caller['line']; + if(!empty($caller['class']) && !empty($caller['function'])) { + $return .= " - {$caller['class']}::{$caller['function']}()"; + } + return $return; + } + + /** + * Outputs a variable in a user presentable way + * + * @param object $val + * @param array $caller Caller information + */ + public function writeVariable($val, $caller) { + echo '';
+ echo "" . $this->formatCaller($caller). " - \n";
+ if (is_string($val)) print_r(wordwrap($val, self::config()->columns));
+ else print_r($val);
+ echo '
';
+ }
}
-
diff --git a/dev/Log.php b/dev/Log.php
index ade21604e..f14ac4d48 100644
--- a/dev/Log.php
+++ b/dev/Log.php
@@ -49,6 +49,8 @@ class SS_Log {
const ERR = Zend_Log::ERR;
const WARN = Zend_Log::WARN;
const NOTICE = Zend_Log::NOTICE;
+ const INFO = Zend_Log::INFO;
+ const DEBUG = Zend_Log::DEBUG;
/**
* Logger class to use.
diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php
index 9a212842b..4bc2a739d 100644
--- a/dev/SapphireTest.php
+++ b/dev/SapphireTest.php
@@ -201,7 +201,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
DataObject::reset();
if(class_exists('SiteTree')) SiteTree::reset();
Hierarchy::reset();
- if(Controller::has_curr()) Controller::curr()->setSession(new Session(array()));
+ if(Controller::has_curr()) Controller::curr()->setSession(Injector::inst()->create('Session', array()));
Security::$database_is_ready = null;
// Add controller-name auto-routing
diff --git a/dev/TestSession.php b/dev/TestSession.php
index afb71eb7f..398bbfe71 100644
--- a/dev/TestSession.php
+++ b/dev/TestSession.php
@@ -23,7 +23,7 @@ class TestSession {
private $lastUrl;
public function __construct() {
- $this->session = new Session(array());
+ $this->session = Injector::inst()->create('Session', array());
$this->controller = new Controller();
$this->controller->setSession($this->session);
$this->controller->pushCurrent();
diff --git a/docs/en/changelogs/3.1.6.md b/docs/en/changelogs/3.1.6.md
new file mode 100644
index 000000000..54d68db30
--- /dev/null
+++ b/docs/en/changelogs/3.1.6.md
@@ -0,0 +1,9 @@
+# 3.1.6
+
+## Upgrading
+
+ * The use of the isDev or isTest query string parameter is now restricted to those logged in as admin,
+ and requires users to login via the front end form rather than using basic authentication. This
+ follows the same process as the use of the 'flush' query string parameter, and will redirect
+ requests containing this argument with a token in the querystring.
+ Director::isDev, Director::isTest, and Director::isLive no longer have any parameters.
diff --git a/docs/en/reference/modeladmin.md b/docs/en/reference/modeladmin.md
index d1c32cdcd..d29cd73bf 100644
--- a/docs/en/reference/modeladmin.md
+++ b/docs/en/reference/modeladmin.md
@@ -9,7 +9,7 @@ It uses the framework's knowledge about the model to provide sensible defaults,
allowing you to get started in a couple of lines of code,
while still providing a solid base for customization.
-The interface is mainly powered by the `[GridField](/reference/grid-field)` class,
+The interface is mainly powered by the [GridField](/reference/grid-field) class,
which can also be used in other CMS areas (e.g. to manage a relation on a `SiteTree`
record in the standard CMS interface).
@@ -17,7 +17,7 @@ record in the standard CMS interface).
Let's assume we want to manage a simple product listing as a sample data model:
A product can have a name, price, and a category.
-
+
:::php
class Product extends DataObject {
private static $db = array('Name' => 'Varchar', 'ProductCode' => 'Varchar', 'Price' => 'Currency');
@@ -74,13 +74,13 @@ less restrictive checks make sense, e.g. checking for general CMS access rights.
## Search Fields
-ModelAdmin uses the `[SearchContext](/reference/searchcontext)` class to provide
+ModelAdmin uses the [SearchContext](/reference/searchcontext) class to provide
a search form, as well as get the searched results. Every DataObject can have its own context,
based on the fields which should be searchable. The class makes a guess at how those fields
should be searched, e.g. showing a checkbox for any boolean fields in your `$db` definition.
-To remove, add or modify searchable fields, define a new `[$searchable_fields](api:DataObject::$searchable_fields)`
-static on your model class (see `[SearchContext](/reference/searchcontext)` docs for details).
+To remove, add or modify searchable fields, define a new `[api:DataObject::$searchable_fields]`
+static on your model class (see [SearchContext](/reference/searchcontext) docs for details).
:::php
class Product extends DataObject {
@@ -93,11 +93,11 @@ static on your model class (see `[SearchContext](/reference/searchcontext)` docs
}
For a more sophisticated customization, for example configuring the form fields
-for the search form, override `[api:DataObject->getCustomSearchContext()]` on your model class.
+for the search form, override `DataObject->getCustomSearchContext()` on your model class.
## Result Columns
-The results are shown in a tabular listing, powered by the `[GridField](/reference/grid-field)`,
+The results are shown in a tabular listing, powered by the [GridField](/reference/grid-field),
more specifically the `[api:GridFieldDataColumns]` component.
It looks for a `[api:DataObject::$summary_fields]` static on your model class,
where you can add or remove columns. To change the title, use `[api:DataObject::$field_labels]`.
@@ -203,10 +203,10 @@ Has-one relationships are simply implemented as a `[api:DropdownField]` by defau
Consider replacing it with a more powerful interface in case you have many records
(through customizing `[api:DataObject->getCMSFields]`).
-Has-many and many-many relationships are usually handled via the `[GridField](/reference/grid-field)` class,
+Has-many and many-many relationships are usually handled via the [GridField](/reference/grid-field) class,
more specifically the `[api:GridFieldAddExistingAutocompleter]` and `[api:GridFieldRelationDelete]` components.
They provide a list/detail interface within a single record edited in your ModelAdmin.
-The `[GridField](/reference/grid-field)` docs also explain how to manage
+The [GridField](/reference/grid-field) docs also explain how to manage
extra relation fields on join tables through its detail forms.
The autocompleter can also search attributes on relations,
based on the search fields defined through `[api:DataObject::searchableFields()]`.
@@ -287,7 +287,7 @@ Interfaces like `ModelAdmin` can be customized in many ways:
* HTML markup through templates
In general, use your `ModelAdmin->init()` method to add additional requirements
-through the `[Requirements](/reference/requirements)` API.
+through the [Requirements](/reference/requirements) API.
For an introduction how to customize the CMS templates, see our [CMS Architecture Guide](/reference/cms-architecture).
## Related
diff --git a/docs/en/reference/templates.md b/docs/en/reference/templates.md
index 385111fb2..da8f0a19a 100644
--- a/docs/en/reference/templates.md
+++ b/docs/en/reference/templates.md
@@ -641,9 +641,9 @@ default if it exists and there is no action in the url parameters.
}
}
-## Fragment Link rewriting
+## Anchor Link rewriting
-Fragment links are links with a "#" in them. A frequent use-case is to use fragment links to point to different
+Anchor links are links with a "#" in them. A frequent use-case is to use anchor links to point to different
sections of the current page. For example, we might have this in our template.
For, example, we might have this on http://www.example.com/my-long-page/
@@ -659,8 +659,8 @@ So far, so obvious. However, things get tricky because of we have set our `