2011-03-18 04:01:06 +01:00
< ? php
2016-06-16 06:57:19 +02:00
2016-07-22 01:32:32 +02:00
namespace SilverStripe\CMS\Model ;
2016-08-23 04:36:06 +02:00
use Page ;
2017-11-30 03:56:16 +01:00
use Psr\SimpleCache\CacheInterface ;
2022-09-30 04:10:49 +02:00
use SilverStripe\Admin\CMSEditLinkExtension ;
2018-04-06 05:53:57 +02:00
use SilverStripe\Assets\Shortcodes\FileLinkTracking ;
2022-09-30 04:10:49 +02:00
use SilverStripe\CMS\Controllers\CMSMain ;
2016-08-10 06:08:39 +02:00
use SilverStripe\CMS\Controllers\CMSPageEditController ;
use SilverStripe\CMS\Controllers\ContentController ;
2016-08-23 04:36:06 +02:00
use SilverStripe\CMS\Controllers\ModelAsController ;
use SilverStripe\CMS\Controllers\RootURLController ;
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField ;
2017-01-23 03:11:49 +01:00
use SilverStripe\Control\ContentNegotiator ;
2016-08-23 04:36:06 +02:00
use SilverStripe\Control\Controller ;
use SilverStripe\Control\Director ;
2018-03-14 04:34:46 +01:00
use SilverStripe\Core\Cache\MemberCacheFlusher ;
2016-08-23 04:36:06 +02:00
use SilverStripe\Core\ClassInfo ;
use SilverStripe\Core\Config\Config ;
use SilverStripe\Core\Convert ;
2017-11-30 03:56:16 +01:00
use SilverStripe\Core\Flushable ;
2017-06-21 06:29:40 +02:00
use SilverStripe\Core\Injector\Injector ;
2017-10-18 01:32:08 +02:00
use SilverStripe\Core\Manifest\ModuleResource ;
use SilverStripe\Core\Manifest\ModuleResourceLoader ;
2022-03-09 02:48:02 +01:00
use SilverStripe\Core\Manifest\VersionProvider ;
2017-01-26 05:21:00 +01:00
use SilverStripe\Core\Resettable ;
2016-08-23 04:36:06 +02:00
use SilverStripe\Dev\Deprecation ;
use SilverStripe\Forms\CheckboxField ;
use SilverStripe\Forms\CompositeField ;
use SilverStripe\Forms\DropdownField ;
use SilverStripe\Forms\FieldGroup ;
use SilverStripe\Forms\FieldList ;
use SilverStripe\Forms\FormAction ;
use SilverStripe\Forms\GridField\GridField ;
use SilverStripe\Forms\GridField\GridFieldDataColumns ;
2018-10-09 03:16:43 +02:00
use SilverStripe\Forms\GridField\GridFieldLazyLoader ;
2016-08-23 04:36:06 +02:00
use SilverStripe\Forms\HTMLEditor\HTMLEditorField ;
use SilverStripe\Forms\LiteralField ;
use SilverStripe\Forms\OptionsetField ;
use SilverStripe\Forms\Tab ;
use SilverStripe\Forms\TabSet ;
use SilverStripe\Forms\TextareaField ;
use SilverStripe\Forms\TextField ;
use SilverStripe\Forms\ToggleCompositeField ;
use SilverStripe\Forms\TreeDropdownField ;
2017-08-30 12:40:56 +02:00
use SilverStripe\Forms\TreeMultiselectField ;
2016-08-23 04:36:06 +02:00
use SilverStripe\i18n\i18n ;
use SilverStripe\i18n\i18nEntityProvider ;
use SilverStripe\ORM\ArrayList ;
2017-06-21 06:29:40 +02:00
use SilverStripe\ORM\CMSPreviewable ;
2016-08-23 04:36:06 +02:00
use SilverStripe\ORM\DataList ;
2016-06-16 06:57:19 +02:00
use SilverStripe\ORM\DataObject ;
2016-08-23 04:36:06 +02:00
use SilverStripe\ORM\DB ;
2018-04-06 05:53:57 +02:00
use SilverStripe\ORM\HasManyList ;
2016-08-23 04:36:06 +02:00
use SilverStripe\ORM\HiddenClass ;
2016-06-16 06:57:19 +02:00
use SilverStripe\ORM\Hierarchy\Hierarchy ;
use SilverStripe\ORM\ManyManyList ;
2016-12-09 04:00:46 +01:00
use SilverStripe\ORM\ValidationResult ;
2017-06-21 06:29:40 +02:00
use SilverStripe\Security\Group ;
2017-05-12 02:47:46 +02:00
use SilverStripe\Security\InheritedPermissions ;
use SilverStripe\Security\InheritedPermissionsExtension ;
2016-06-23 01:51:20 +02:00
use SilverStripe\Security\Member ;
use SilverStripe\Security\Permission ;
2017-06-21 06:29:40 +02:00
use SilverStripe\Security\PermissionChecker ;
2016-06-23 01:51:20 +02:00
use SilverStripe\Security\PermissionProvider ;
2017-06-21 06:29:40 +02:00
use SilverStripe\Security\Security ;
2016-08-23 04:36:06 +02:00
use SilverStripe\SiteConfig\SiteConfig ;
2018-09-27 14:07:42 +02:00
use SilverStripe\Subsites\Model\Subsite ;
2018-03-14 04:34:46 +01:00
use SilverStripe\Versioned\RecursivePublishable ;
2017-06-21 06:29:40 +02:00
use SilverStripe\Versioned\Versioned ;
2016-08-23 04:36:06 +02:00
use SilverStripe\View\ArrayData ;
2017-04-18 00:52:51 +02:00
use SilverStripe\View\HTML ;
2022-08-23 06:44:58 +02:00
use SilverStripe\View\Parsers\HTMLValue ;
2016-08-23 04:36:06 +02:00
use SilverStripe\View\Parsers\ShortcodeParser ;
use SilverStripe\View\Parsers\URLSegmentFilter ;
2020-08-26 00:13:39 +02:00
use SilverStripe\View\Shortcodes\EmbedShortcodeProvider ;
2016-08-23 04:36:06 +02:00
use SilverStripe\View\SSViewer ;
2016-06-16 06:57:19 +02:00
2011-03-18 04:01:06 +01:00
/**
2014-12-01 12:22:48 +01:00
* Basic data - object representing all pages within the site tree . All page types that live within the hierarchy should
* inherit from this . In addition , it contains a number of static methods for querying the site tree and working with
* draft and published states .
2016-01-06 00:42:07 +01:00
*
2012-02-06 11:58:04 +01:00
* < h2 > URLs </ h2 >
2014-12-01 12:22:48 +01:00
* A page is identified during request handling via its " URLSegment " database column . As pages can be nested , the full
* path of a URL might contain multiple segments . Each segment is stored in its filtered representation ( through
* { @ link URLSegmentFilter }) . The full path is constructed via { @ link Link ()}, { @ link RelativeLink ()} and
* { @ link AbsoluteLink ()} . You can allow these segments to contain multibyte characters through
* { @ link URLSegmentFilter :: $default_allow_multibyte } .
2014-02-10 00:20:55 +01:00
*
2018-04-06 05:53:57 +02:00
* @ property string $URLSegment
* @ property string $Title
* @ property string $MenuTitle
* @ property string $Content HTML content of the page .
* @ property string $MetaDescription
* @ property string $ExtraMeta
* @ property string $ReportClass
2020-05-01 01:01:51 +02:00
* @ property int $Sort Integer value denoting the sort order .
* @ property bool $ShowInMenus
* @ property bool $ShowInSearch
2018-04-06 05:53:57 +02:00
* @ property bool $HasBrokenFile True if this page has a broken file shortcode
* @ property bool $HasBrokenLink True if this page has a broken page shortcode
2014-02-10 00:20:55 +01:00
*
2016-08-11 04:10:51 +02:00
* @ method ManyManyList ViewerGroups () List of groups that can view this object .
* @ method ManyManyList EditorGroups () List of groups that can edit this object .
2016-08-10 06:08:39 +02:00
* @ method SiteTree Parent ()
2018-04-06 05:53:57 +02:00
* @ method HasManyList | SiteTreeLink [] BackLinks () List of SiteTreeLink objects attached to this page
2014-12-01 12:22:48 +01:00
*
* @ mixin Hierarchy
* @ mixin Versioned
2018-03-14 04:34:46 +01:00
* @ mixin RecursivePublishable
2018-04-06 05:53:57 +02:00
* @ mixin SiteTreeLinkTracking Added via linktracking . yml to DataObject directly
* @ mixin FileLinkTracking Added via filetracking . yml in silverstripe / assets
2017-05-12 02:47:46 +02:00
* @ mixin InheritedPermissionsExtension
2011-03-18 04:01:06 +01:00
*/
2017-11-30 03:56:16 +01:00
class SiteTree extends DataObject implements PermissionProvider , i18nEntityProvider , CMSPreviewable , Resettable , Flushable , MemberCacheFlusher
2017-01-25 21:59:25 +01:00
{
/**
* Indicates what kind of children this page type can have .
* This can be an array of allowed child classes , or the string " none " -
* indicating that this page type can ' t have children .
* If a classname is prefixed by " * " , such as " *Page " , then only that
* class is allowed - no subclasses . Otherwise , the class and all its
* subclasses are allowed .
* To control allowed children on root level ( no parent ), use { @ link $can_be_root } .
*
* Note that this setting is cached when used in the CMS , use the " flush " query parameter to clear it .
*
* @ config
* @ var array
*/
private static $allowed_children = [
self :: class
];
2017-02-10 11:31:25 +01:00
/**
* Used as a cache for `self::allowedChildren()`
* Drastically reduces admin page load when there are a lot of page types
* @ var array
*/
2020-04-19 06:18:01 +02:00
protected static $_allowedChildren = [];
2017-02-10 11:31:25 +01:00
2019-10-30 10:02:18 +01:00
/**
* Determines if the Draft Preview panel will appear when in the CMS admin
* @ var bool
*/
2019-10-29 18:43:17 +01:00
private static $show_stage_link = true ;
2019-10-30 10:02:18 +01:00
/**
* Determines if the Live Preview panel will appear when in the CMS admin
* @ var bool
*/
2019-10-29 18:43:17 +01:00
private static $show_live_link = true ;
2017-01-25 21:59:25 +01:00
/**
* The default child class for this page .
* Note : Value might be cached , see { @ link $allowed_chilren } .
*
* @ config
* @ var string
*/
private static $default_child = " Page " ;
/**
* Default value for SiteTree . ClassName enum
* { @ see DBClassName :: getDefault }
*
* @ config
* @ var string
*/
private static $default_classname = " Page " ;
/**
* The default parent class for this page .
* Note : Value might be cached , see { @ link $allowed_chilren } .
*
* @ config
* @ var string
*/
private static $default_parent = null ;
/**
* Controls whether a page can be in the root of the site tree .
* Note : Value might be cached , see { @ link $allowed_chilren } .
*
* @ config
* @ var bool
*/
private static $can_be_root = true ;
/**
* List of permission codes a user can have to allow a user to create a page of this type .
* Note : Value might be cached , see { @ link $allowed_chilren } .
*
* @ config
* @ var array
*/
private static $need_permission = null ;
/**
* If you extend a class , and don ' t want to be able to select the old class
* in the cms , set this to the old class name . Eg , if you extended Product
* to make ImprovedProduct , then you would set $hide_ancestor to Product .
*
* @ config
* @ var string
*/
private static $hide_ancestor = null ;
2018-03-01 16:31:50 +01:00
/**
* You can define the class of the controller that maps to your SiteTree object here if
* you don ' t want to rely on the magic of appending Controller to the Classname
*
* @ config
* @ var string
*/
private static $controller_name = null ;
2022-09-30 04:10:49 +02:00
/**
* The class of the LeftAndMain controller where this class is managed .
* @ see CMSEditLinkExtension :: getCMSEditOwner ()
*/
private static string $cms_edit_owner = CMSMain :: class ;
2019-10-15 13:29:05 +02:00
/**
* You can define the a map of Page namespaces to Controller namespaces here
* This will apply after the magic of appending Controller , and in order
* Must be applied to SiteTree config e . g .
*
* SilverStripe\CMS\Model\SiteTree :
* namespace_map :
* " App \ Pages " : " App \ Control "
*
* Will map App\Pages\MyPage to App\Control\MyPageController
*
* @ config
* @ var string
*/
private static $namespace_map = null ;
2020-04-19 06:18:01 +02:00
private static $db = [
2017-01-25 21:59:25 +01:00
" URLSegment " => " Varchar(255) " ,
" Title " => " Varchar(255) " ,
" MenuTitle " => " Varchar(100) " ,
" Content " => " HTMLText " ,
" MetaDescription " => " Text " ,
" ExtraMeta " => " HTMLFragment(['whitelist' => ['meta', 'link']]) " ,
" ShowInMenus " => " Boolean " ,
" ShowInSearch " => " Boolean " ,
" Sort " => " Int " ,
" HasBrokenFile " => " Boolean " ,
" HasBrokenLink " => " Boolean " ,
" ReportClass " => " Varchar " ,
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
2020-04-19 06:18:01 +02:00
private static $indexes = [
2017-01-25 21:59:25 +01:00
" URLSegment " => true ,
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
2018-04-06 05:53:57 +02:00
private static $has_many = [
" VirtualPages " => VirtualPage :: class . '.CopyContentFrom' ,
'BackLinks' => SiteTreeLink :: class . '.Linked' ,
];
2017-01-25 21:59:25 +01:00
2020-04-19 06:18:01 +02:00
private static $owned_by = [
2017-01-25 21:59:25 +01:00
" VirtualPages "
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
2017-08-09 00:56:08 +02:00
private static $cascade_deletes = [
'VirtualPages' ,
];
2020-04-19 06:18:01 +02:00
private static $casting = [
2017-01-25 21:59:25 +01:00
" Breadcrumbs " => " HTMLFragment " ,
" LastEdited " => " Datetime " ,
" Created " => " Datetime " ,
'Link' => 'Text' ,
'RelativeLink' => 'Text' ,
'AbsoluteLink' => 'Text' ,
'CMSEditLink' => 'Text' ,
'TreeTitle' => 'HTMLFragment' ,
'MetaTags' => 'HTMLFragment' ,
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
2020-04-19 06:18:01 +02:00
private static $defaults = [
2017-01-25 21:59:25 +01:00
" ShowInMenus " => 1 ,
" ShowInSearch " => 1 ,
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
private static $table_name = 'SiteTree' ;
2020-04-19 06:18:01 +02:00
private static $versioning = [
2017-01-25 21:59:25 +01:00
" Stage " , " Live "
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
private static $default_sort = " \" Sort \" " ;
/**
* If this is false , the class cannot be created in the CMS by regular content authors , only by ADMINs .
* @ var boolean
* @ config
*/
private static $can_create = true ;
/**
* Icon to use in the CMS page tree . This should be the full filename , relative to the webroot .
* Also supports custom CSS rule contents ( applied to the correct selector for the tree UI implementation ) .
*
2017-10-18 06:23:39 +02:00
* @ see LeftAndMainPageIconsExtension :: generatePageIconsCss ()
2017-01-25 21:59:25 +01:00
* @ config
* @ var string
*/
private static $icon = null ;
2019-01-14 01:16:30 +01:00
/**
* Class attached to page icons in the CMS page tree . Also supports font - icon set .
* @ config
* @ var string
*/
private static $icon_class = 'font-icon-page' ;
2017-01-25 21:59:25 +01:00
private static $extensions = [
Hierarchy :: class ,
Versioned :: class ,
2017-05-12 02:47:46 +02:00
InheritedPermissionsExtension :: class ,
2022-09-30 04:10:49 +02:00
CMSEditLinkExtension :: class ,
2017-01-25 21:59:25 +01:00
];
2020-04-19 06:18:01 +02:00
private static $searchable_fields = [
2017-01-25 21:59:25 +01:00
'Title' ,
'Content' ,
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
2020-04-19 06:18:01 +02:00
private static $field_labels = [
2017-01-25 21:59:25 +01:00
'URLSegment' => 'URL'
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
/**
* @ config
*/
private static $nested_urls = true ;
/**
* @ config
*/
private static $create_default_pages = true ;
/**
* This controls whether of not extendCMSFields () is called by getCMSFields .
*/
private static $runCMSFieldsExtensions = true ;
/**
2020-09-24 09:27:52 +02:00
* Deleting this page also deletes all its children when set to true .
*
2017-01-25 21:59:25 +01:00
* @ config
* @ var boolean
*/
private static $enforce_strict_hierarchy = true ;
/**
* The value used for the meta generator tag . Leave blank to omit the tag .
*
* @ config
* @ var string
*/
2022-03-09 02:48:02 +01:00
private static $meta_generator = 'Silverstripe CMS' ;
/**
* Whether to display the version portion of the meta generator tag
* Set to false if it ' s viewed as a concern .
*
* @ config
* @ var bool
*/
private static $show_meta_generator_version = true ;
2017-01-25 21:59:25 +01:00
protected $_cache_statusFlags = null ;
/**
* Plural form for SiteTree / Page classes . Not inherited by subclasses .
*
* @ config
* @ var string
*/
private static $base_plural_name = 'Pages' ;
/**
* Plural form for SiteTree / Page classes . Not inherited by subclasses .
*
* @ config
* @ var string
*/
private static $base_singular_name = 'Page' ;
/**
* Description of the class functionality , typically shown to a user
* when selecting which page type to create . Translated through { @ link provideI18nEntities ()} .
*
2017-03-29 00:55:44 +02:00
* @ see SiteTree :: classDescription ()
* @ see SiteTree :: i18n_classDescription ()
2017-01-25 21:59:25 +01:00
*
* @ config
* @ var string
*/
private static $description = null ;
/**
* Description for Page and SiteTree classes , but not inherited by subclasses .
* override SiteTree :: $description in subclasses instead .
*
2017-03-29 00:55:44 +02:00
* @ see SiteTree :: classDescription ()
* @ see SiteTree :: i18n_classDescription ()
2017-01-25 21:59:25 +01:00
*
* @ config
* @ var string
*/
private static $base_description = 'Generic content page' ;
2017-11-30 03:56:16 +01:00
/**
* @ var array
*/
private static $dependencies = [
'creatableChildrenCache' => '%$' . CacheInterface :: class . '.SiteTree_CreatableChildren'
];
/**
* @ var CacheInterface
*/
protected $creatableChildrenCache ;
2022-03-09 02:48:02 +01:00
/**
* @ var VersionProvider
*/
private $versionProvider ;
2017-01-25 21:59:25 +01:00
/**
* Fetches the { @ link SiteTree } object that maps to a link .
*
* If you have enabled { @ link SiteTree :: config () -> nested_urls } on this site , then you can use a nested link such as
* " about-us/staff/ " , and this function will traverse down the URL chain and grab the appropriate link .
*
* Note that if no model can be found , this method will fall over to a extended alternateGetByLink method provided
* by a extension attached to { @ link SiteTree }
*
* @ param string $link The link of the page to search for
* @ param bool $cache True ( default ) to use caching , false to force a fresh search from the database
2021-07-17 01:39:18 +02:00
* @ return SiteTree | null
2017-01-25 21:59:25 +01:00
*/
public static function get_by_link ( $link , $cache = true )
{
2019-05-17 03:40:15 +02:00
// Compute the column names with dynamic a dynamic table name
$tableName = DataObject :: singleton ( self :: class ) -> baseTable ();
$urlSegmentExpr = sprintf ( '"%s"."URLSegment"' , $tableName );
$parentIDExpr = sprintf ( '"%s"."ParentID"' , $tableName );
2022-04-13 07:07:59 +02:00
$link = trim ( Director :: makeRelative ( $link ) ? ? '' , '/' );
2021-09-21 07:09:48 +02:00
if ( ! $link ) {
2017-01-25 21:59:25 +01:00
$link = RootURLController :: get_homepage_link ();
}
2022-04-13 07:07:59 +02:00
$parts = preg_split ( '|/+|' , $link ? ? '' );
2017-01-25 21:59:25 +01:00
// Grab the initial root level page to traverse down from.
$URLSegment = array_shift ( $parts );
2022-04-13 07:07:59 +02:00
$conditions = [ $urlSegmentExpr => rawurlencode ( $URLSegment ? ? '' )];
2018-04-06 05:53:57 +02:00
if ( self :: config () -> get ( 'nested_urls' )) {
2020-04-19 06:18:01 +02:00
$conditions [] = [ $parentIDExpr => 0 ];
2017-01-25 21:59:25 +01:00
}
/** @var SiteTree $sitetree */
$sitetree = DataObject :: get_one ( self :: class , $conditions , $cache );
/// Fall back on a unique URLSegment for b/c.
if ( ! $sitetree
2018-04-06 05:53:57 +02:00
&& self :: config () -> get ( 'nested_urls' )
2020-04-19 06:18:01 +02:00
&& $sitetree = DataObject :: get_one ( self :: class , [
2019-05-17 03:40:15 +02:00
$urlSegmentExpr => $URLSegment
2020-04-19 06:18:01 +02:00
], $cache )
2017-01-25 21:59:25 +01:00
) {
return $sitetree ;
}
// Attempt to grab an alternative page from extensions.
if ( ! $sitetree ) {
2018-04-06 05:53:57 +02:00
$parentID = self :: config () -> get ( 'nested_urls' ) ? 0 : null ;
2017-01-25 21:59:25 +01:00
if ( $alternatives = static :: singleton () -> extend ( 'alternateGetByLink' , $URLSegment , $parentID )) {
foreach ( $alternatives as $alternative ) {
if ( $alternative ) {
$sitetree = $alternative ;
}
}
}
if ( ! $sitetree ) {
return null ;
}
}
// Check if we have any more URL parts to parse.
2022-04-13 07:07:59 +02:00
if ( ! self :: config () -> get ( 'nested_urls' ) || ! count ( $parts ? ? [])) {
2017-01-25 21:59:25 +01:00
return $sitetree ;
}
// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
foreach ( $parts as $segment ) {
$next = DataObject :: get_one (
self :: class ,
2020-04-19 06:18:01 +02:00
[
2019-05-17 03:40:15 +02:00
$urlSegmentExpr => $segment ,
$parentIDExpr => $sitetree -> ID
2020-04-19 06:18:01 +02:00
],
2017-01-25 21:59:25 +01:00
$cache
);
if ( ! $next ) {
$parentID = ( int ) $sitetree -> ID ;
if ( $alternatives = static :: singleton () -> extend ( 'alternateGetByLink' , $segment , $parentID )) {
foreach ( $alternatives as $alternative ) {
if ( $alternative ) {
$next = $alternative ;
}
}
}
if ( ! $next ) {
return null ;
}
}
$sitetree -> destroy ();
$sitetree = $next ;
}
return $sitetree ;
}
/**
* Return a subclass map of SiteTree that shouldn ' t be hidden through { @ link SiteTree :: $hide_ancestor }
*
* @ return array
*/
public static function page_type_classes ()
{
$classes = ClassInfo :: getValidSubClasses ();
2022-04-13 07:07:59 +02:00
$baseClassIndex = array_search ( self :: class , $classes ? ? []);
2017-01-25 21:59:25 +01:00
if ( $baseClassIndex !== false ) {
unset ( $classes [ $baseClassIndex ]);
}
2020-04-19 06:18:01 +02:00
$kill_ancestors = [];
2017-01-25 21:59:25 +01:00
// figure out if there are any classes we don't want to appear
foreach ( $classes as $class ) {
$instance = singleton ( $class );
// do any of the progeny want to hide an ancestor?
2017-08-22 23:46:46 +02:00
if ( $ancestor_to_hide = $instance -> config () -> get ( 'hide_ancestor' )) {
2017-01-25 21:59:25 +01:00
// note for killing later
$kill_ancestors [] = $ancestor_to_hide ;
}
}
// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
// requirements
if ( $kill_ancestors ) {
2022-04-13 07:07:59 +02:00
$kill_ancestors = array_unique ( $kill_ancestors ? ? []);
2017-01-25 21:59:25 +01:00
foreach ( $kill_ancestors as $mark ) {
// unset from $classes
2022-04-13 07:07:59 +02:00
$idx = array_search ( $mark , $classes ? ? [], true );
2017-01-25 21:59:25 +01:00
if ( $idx !== false ) {
unset ( $classes [ $idx ]);
}
}
}
return $classes ;
}
/**
* Replace a " [sitetree_link id=n] " shortcode with a link to the page with the corresponding ID .
*
* @ param array $arguments
* @ param string $content
* @ param ShortcodeParser $parser
* @ return string
*/
public static function link_shortcode_handler ( $arguments , $content = null , $parser = null )
{
if ( ! isset ( $arguments [ 'id' ]) || ! is_numeric ( $arguments [ 'id' ])) {
return null ;
}
/** @var SiteTree $page */
if ( ! ( $page = DataObject :: get_by_id ( self :: class , $arguments [ 'id' ])) // Get the current page by ID.
&& ! ( $page = Versioned :: get_latest_version ( self :: class , $arguments [ 'id' ])) // Attempt link to old version.
) {
2018-07-15 21:44:23 +02:00
return null ; // There were no suitable matches at all.
2017-01-25 21:59:25 +01:00
}
/** @var SiteTree $page */
$link = Convert :: raw2att ( $page -> Link ());
if ( $content ) {
return sprintf ( '<a href="%s">%s</a>' , $link , $parser -> parse ( $content ));
} else {
return $link ;
}
}
/**
* Return the link for this { @ link SiteTree } object , with the { @ link Director :: baseURL ()} included .
*
* @ param string $action Optional controller action ( method ) .
* Note : URI encoding of this parameter is applied automatically through template casting ,
* don ' t encode the passed parameter . Please use { @ link Controller :: join_links ()} instead to
* append GET parameters .
* @ return string
*/
public function Link ( $action = null )
{
2017-09-04 09:48:21 +02:00
$relativeLink = $this -> RelativeLink ( $action );
$link = Controller :: join_links ( Director :: baseURL (), $relativeLink );
$this -> extend ( 'updateLink' , $link , $action , $relativeLink );
return $link ;
2017-01-25 21:59:25 +01:00
}
/**
* Get the absolute URL for this page , including protocol and host .
*
* @ param string $action See { @ link Link ()}
* @ return string
*/
public function AbsoluteLink ( $action = null )
{
if ( $this -> hasMethod ( 'alternateAbsoluteLink' )) {
return $this -> alternateAbsoluteLink ( $action );
} else {
return Director :: absoluteURL ( $this -> Link ( $action ));
}
}
/**
* Base link used for previewing . Defaults to absolute URL , in order to account for domain changes , e . g . on multi
* site setups . Does not contain hints about the stage , see { @ link SilverStripeNavigator } for details .
*
* @ param string $action See { @ link Link ()}
* @ return string
*/
public function PreviewLink ( $action = null )
{
if ( $this -> hasMethod ( 'alternatePreviewLink' )) {
2022-11-24 01:03:26 +01:00
Deprecation :: withNoReplacement ( function () use ( $action ) {
Deprecation :: notice ( '5.0' , 'Use updatePreviewLink or override PreviewLink method' );
return $this -> alternatePreviewLink ( $action );
});
2017-01-25 21:59:25 +01:00
}
$link = $this -> AbsoluteLink ( $action );
$this -> extend ( 'updatePreviewLink' , $link , $action );
return $link ;
}
public function getMimeType ()
{
return 'text/html' ;
}
/**
* Return the link for this { @ link SiteTree } object relative to the SilverStripe root .
*
* By default , if this page is the current home page , and there is no action specified then this will return a link
* to the root of the site . However , if you set the $action parameter to TRUE then the link will not be rewritten
* and returned in its full form .
*
* @ uses RootURLController :: get_homepage_link ()
*
* @ param string $action See { @ link Link ()}
* @ return string
*/
public function RelativeLink ( $action = null )
{
2018-04-06 05:53:57 +02:00
if ( $this -> ParentID && self :: config () -> get ( 'nested_urls' )) {
2017-01-25 21:59:25 +01:00
$parent = $this -> Parent ();
// If page is removed select parent from version history (for archive page view)
if (( ! $parent || ! $parent -> exists ()) && ! $this -> isOnDraft ()) {
$parent = Versioned :: get_latest_version ( self :: class , $this -> ParentID );
}
2019-12-04 05:08:49 +01:00
$base = $parent ? $parent -> RelativeLink ( $this -> URLSegment ) : null ;
2017-01-25 21:59:25 +01:00
} elseif ( ! $action && $this -> URLSegment == RootURLController :: get_homepage_link ()) {
// Unset base for root-level homepages.
// Note: Homepages with action parameters (or $action === true)
// need to retain their URLSegment.
$base = null ;
} else {
$base = $this -> URLSegment ;
}
$this -> extend ( 'updateRelativeLink' , $base , $action );
// Legacy support: If $action === true, retain URLSegment for homepages,
// but don't append any action
if ( $action === true ) {
$action = null ;
}
return Controller :: join_links ( $base , '/' , $action );
}
/**
* Get the absolute URL for this page on the Live site .
*
* @ param bool $includeStageEqualsLive Whether to append the URL with ? stage = Live to force Live mode
* @ return string
*/
public function getAbsoluteLiveLink ( $includeStageEqualsLive = true )
{
$oldReadingMode = Versioned :: get_reading_mode ();
Versioned :: set_stage ( Versioned :: LIVE );
2019-05-17 03:40:15 +02:00
$tablename = $this -> baseTable ();
2017-01-25 21:59:25 +01:00
/** @var SiteTree $live */
2020-04-19 06:18:01 +02:00
$live = Versioned :: get_one_by_stage ( self :: class , Versioned :: LIVE , [
2019-05-17 03:40:15 +02:00
" \" $tablename\ " . \ " ID \" " => $this -> ID
2020-04-19 06:18:01 +02:00
]);
2017-01-25 21:59:25 +01:00
if ( $live ) {
$link = $live -> AbsoluteLink ();
if ( $includeStageEqualsLive ) {
$link = Controller :: join_links ( $link , '?stage=Live' );
}
} else {
$link = null ;
}
Versioned :: set_reading_mode ( $oldReadingMode );
return $link ;
}
/**
* Generates a link to edit this page in the CMS .
*
* @ return string
*/
public function CMSEditLink ()
{
2022-09-30 04:10:49 +02:00
// This method has to be implemented here to satisfy the CMSPreviewable interface.
// See the actual implementation in CMSEditLinkExtension.
return $this -> extend ( 'CMSEditLink' )[ 0 ];
2017-01-25 21:59:25 +01:00
}
/**
* Return a CSS identifier generated from this page ' s link .
*
* @ return string The URL segment
*/
public function ElementName ()
{
2022-04-13 07:07:59 +02:00
return str_replace ( '/' , '-' , trim ( $this -> RelativeLink ( true ) ? ? '' , '/' ));
2017-01-25 21:59:25 +01:00
}
/**
* Returns true if this is the currently active page being used to handle this request .
*
* @ return bool
*/
public function isCurrent ()
{
$currentPage = Director :: get_current_page ();
if ( $currentPage instanceof ContentController ) {
$currentPage = $currentPage -> data ();
}
if ( $currentPage instanceof SiteTree ) {
return $currentPage === $this || $currentPage -> ID === $this -> ID ;
}
return false ;
}
/**
* Check if this page is in the currently active section ( e . g . it is either current or one of its children is
* currently being viewed ) .
*
* @ return bool
*/
public function isSection ()
{
return $this -> isCurrent () || (
2022-04-13 07:07:59 +02:00
Director :: get_current_page () instanceof SiteTree && in_array ( $this -> ID , Director :: get_current_page () -> getAncestors () -> column () ? ? [])
2017-01-25 21:59:25 +01:00
);
}
/**
* Check if the parent of this page has been removed ( or made otherwise unavailable ), and is still referenced by
* this child . Any such orphaned page may still require access via the CMS , but should not be shown as accessible
* to external users .
*
* @ return bool
*/
public function isOrphaned ()
{
// Always false for root pages
if ( empty ( $this -> ParentID )) {
return false ;
}
// Parent must exist and not be an orphan itself
$parent = $this -> Parent ();
return ! $parent || ! $parent -> exists () || $parent -> isOrphaned ();
}
/**
* Return " link " or " current " depending on if this is the { @ link SiteTree :: isCurrent ()} current page .
*
* @ return string
*/
public function LinkOrCurrent ()
{
return $this -> isCurrent () ? 'current' : 'link' ;
}
/**
* Return " link " or " section " depending on if this is the { @ link SiteTree :: isSeciton ()} current section .
*
* @ return string
*/
public function LinkOrSection ()
{
return $this -> isSection () ? 'section' : 'link' ;
}
/**
* Return " link " , " current " or " section " depending on if this page is the current page , or not on the current page
* but in the current section .
*
* @ return string
*/
public function LinkingMode ()
{
if ( $this -> isCurrent ()) {
return 'current' ;
} elseif ( $this -> isSection ()) {
return 'section' ;
} else {
return 'link' ;
}
}
/**
* Check if this page is in the given current section .
*
* @ param string $sectionName Name of the section to check
* @ return bool True if we are in the given section
*/
public function InSection ( $sectionName )
{
$page = Director :: get_current_page ();
2018-03-11 22:31:40 +01:00
while ( $page instanceof SiteTree && $page -> exists ()) {
if ( $sectionName === $page -> URLSegment ) {
2017-01-25 21:59:25 +01:00
return true ;
}
$page = $page -> Parent ();
}
return false ;
}
/**
* Reset Sort on duped page
*
* @ param SiteTree $original
* @ param bool $doWrite
*/
public function onBeforeDuplicate ( $original , $doWrite )
{
$this -> Sort = 0 ;
}
/**
* Duplicates each child of this node recursively and returns the top - level duplicate node .
*
* @ return static The duplicated object
*/
public function duplicateWithChildren ()
{
/** @var SiteTree $clone */
$clone = $this -> duplicate ();
$children = $this -> AllChildren ();
if ( $children ) {
/** @var SiteTree $child */
$sort = 0 ;
foreach ( $children as $child ) {
2017-06-28 15:35:21 +02:00
$childClone = method_exists ( $child , 'duplicateWithChildren' )
? $child -> duplicateWithChildren ()
: $child -> duplicate ();
2017-01-25 21:59:25 +01:00
$childClone -> ParentID = $clone -> ID ;
//retain sort order by manually setting sort values
$childClone -> Sort = ++ $sort ;
$childClone -> write ();
}
}
return $clone ;
}
/**
* Duplicate this node and its children as a child of the node with the given ID
*
* @ param int $id ID of the new node ' s new parent
*/
public function duplicateAsChild ( $id )
{
/** @var SiteTree $newSiteTree */
$newSiteTree = $this -> duplicate ();
$newSiteTree -> ParentID = $id ;
$newSiteTree -> Sort = 0 ;
$newSiteTree -> write ();
}
/**
* Return a breadcrumb trail to this page . Excludes " hidden " pages ( with ShowInMenus = 0 ) by default .
*
* @ param int $maxDepth The maximum depth to traverse .
* @ param boolean $unlinked Whether to link page titles .
* @ param boolean | string $stopAtPageType ClassName of a page to stop the upwards traversal .
* @ param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
2018-04-06 05:53:57 +02:00
* @ param string $delimiter Delimiter character ( raw html )
2017-01-25 21:59:25 +01:00
* @ return string The breadcrumb trail .
*/
2017-07-19 01:52:19 +02:00
public function Breadcrumbs ( $maxDepth = 20 , $unlinked = false , $stopAtPageType = false , $showHidden = false , $delimiter = '»' )
2017-01-25 21:59:25 +01:00
{
$pages = $this -> getBreadcrumbItems ( $maxDepth , $stopAtPageType , $showHidden );
2017-07-13 05:45:35 +02:00
$template = SSViewer :: create ( 'BreadcrumbsTemplate' );
2020-04-19 06:18:01 +02:00
return $template -> process ( $this -> customise ( new ArrayData ([
2017-01-25 21:59:25 +01:00
" Pages " => $pages ,
2017-07-19 01:52:19 +02:00
" Unlinked " => $unlinked ,
" Delimiter " => $delimiter ,
2020-04-19 06:18:01 +02:00
])));
2017-01-25 21:59:25 +01:00
}
/**
* Returns a list of breadcrumbs for the current page .
*
* @ param int $maxDepth The maximum depth to traverse .
* @ param boolean | string $stopAtPageType ClassName of a page to stop the upwards traversal .
* @ param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
*
* @ return ArrayList
*/
public function getBreadcrumbItems ( $maxDepth = 20 , $stopAtPageType = false , $showHidden = false )
{
$page = $this ;
2020-04-19 06:18:01 +02:00
$pages = [];
2017-01-25 21:59:25 +01:00
while ( $page
&& $page -> exists ()
2022-04-13 07:07:59 +02:00
&& ( ! $maxDepth || count ( $pages ? ? []) < $maxDepth )
2017-01-25 21:59:25 +01:00
&& ( ! $stopAtPageType || $page -> ClassName != $stopAtPageType )
) {
if ( $showHidden || $page -> ShowInMenus || ( $page -> ID == $this -> ID )) {
$pages [] = $page ;
}
$page = $page -> Parent ();
}
2022-04-13 07:07:59 +02:00
return new ArrayList ( array_reverse ( $pages ? ? []));
2017-01-25 21:59:25 +01:00
}
/**
* Make this page a child of another page .
*
* If the parent page does not exist , resolve it to a valid ID before updating this page ' s reference .
*
* @ param SiteTree | int $item Either the parent object , or the parent ID
*/
public function setParent ( $item )
{
if ( is_object ( $item )) {
if ( ! $item -> exists ()) {
$item -> write ();
}
$this -> setField ( " ParentID " , $item -> ID );
} else {
$this -> setField ( " ParentID " , $item );
}
}
/**
* Get the parent of this page .
*
* @ return SiteTree Parent of this page
*/
public function getParent ()
{
2018-04-06 05:53:57 +02:00
$parentID = $this -> getField ( " ParentID " );
if ( $parentID ) {
return SiteTree :: get_by_id ( self :: class , $parentID );
2017-01-25 21:59:25 +01:00
}
return null ;
}
2017-11-30 03:56:16 +01:00
/**
* @ param CacheInterface $cache
* @ return $this
*/
public function setCreatableChildrenCache ( CacheInterface $cache )
{
$this -> creatableChildrenCache = $cache ;
return $this ;
}
/**
* @ return CacheInterface $cache
*/
public function getCreatableChildrenCache ()
{
return $this -> creatableChildrenCache ;
}
2017-01-25 21:59:25 +01:00
/**
* Return a string of the form " parent - page " or " grandparent - parent - page " using page titles
*
* @ param int $level The maximum amount of levels to traverse .
* @ param string $separator Seperating string
* @ return string The resulting string
*/
public function NestedTitle ( $level = 2 , $separator = " - " )
{
$item = $this ;
$parts = [];
while ( $item && $level > 0 ) {
$parts [] = $item -> Title ;
$item = $item -> getParent ();
$level -- ;
}
2022-04-13 07:07:59 +02:00
return implode ( $separator ? ? '' , array_reverse ( $parts ? ? []));
2017-01-25 21:59:25 +01:00
}
/**
* This function should return true if the current user can execute this action . It can be overloaded to customise
* the security model for an application .
*
* Slightly altered from parent behaviour in { @ link DataObject -> can ()} :
* - Checks for existence of a method named " can< $perm >() " on the object
* - Calls decorators and only returns for FALSE " vetoes "
* - Falls back to { @ link Permission :: check ()}
* - Does NOT check for many - many relations named " Can< $perm > "
*
* @ uses DataObjectDecorator -> can ()
*
* @ param string $perm The permission to be checked , such as 'View'
* @ param Member $member The member whose permissions need checking . Defaults to the currently logged in user .
* @ param array $context Context argument for canCreate ()
* @ return bool True if the the member is allowed to do the given action
*/
2020-04-19 06:18:01 +02:00
public function can ( $perm , $member = null , $context = [])
2017-01-25 21:59:25 +01:00
{
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
if ( $member && Permission :: checkMember ( $member , " ADMIN " )) {
return true ;
}
2022-04-13 07:07:59 +02:00
if ( is_string ( $perm ) && method_exists ( $this , 'can' . ucfirst ( $perm ? ? '' ))) {
$method = 'can' . ucfirst ( $perm ? ? '' );
2017-01-25 21:59:25 +01:00
return $this -> $method ( $member );
}
$results = $this -> extend ( 'can' , $member );
if ( $results && is_array ( $results )) {
if ( ! min ( $results )) {
return false ;
}
}
return ( $member && Permission :: checkMember ( $member , $perm ));
}
/**
* This function should return true if the current user can add children to this page . It can be overloaded to
* customise the security model for an application .
*
* Denies permission if any of the following conditions is true :
* - alternateCanAddChildren () on a extension returns false
* - canEdit () is not granted
* - There are no classes defined in { @ link $allowed_children }
*
* @ uses SiteTreeExtension -> canAddChildren ()
* @ uses canEdit ()
* @ uses $allowed_children
*
* @ param Member | int $member
* @ return bool True if the current user can add children
*/
public function canAddChildren ( $member = null )
{
// Disable adding children to archived pages
if ( ! $this -> isOnDraft ()) {
return false ;
}
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Standard mechanism for accepting permission changes from extensions
$extended = $this -> extendedCan ( 'canAddChildren' , $member );
if ( $extended !== null ) {
return $extended ;
}
// Default permissions
if ( $member && Permission :: checkMember ( $member , " ADMIN " )) {
return true ;
}
2017-08-22 23:46:46 +02:00
return $this -> canEdit ( $member ) && $this -> config () -> get ( 'allowed_children' ) !== 'none' ;
2017-01-25 21:59:25 +01:00
}
/**
* This function should return true if the current user can view this page . It can be overloaded to customise the
* security model for an application .
*
* Denies permission if any of the following conditions is true :
* - canView () on any extension returns false
* - " CanViewType " directive is set to " Inherit " and any parent page return false for canView ()
* - " CanViewType " directive is set to " LoggedInUsers " and no user is logged in
* - " CanViewType " directive is set to " OnlyTheseUsers " and user is not in the given groups
*
* @ uses DataExtension -> canView ()
* @ uses ViewerGroups ()
*
2017-05-12 02:47:46 +02:00
* @ param Member $member
2017-01-25 21:59:25 +01:00
* @ return bool True if the current user can view this page
*/
public function canView ( $member = null )
{
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Standard mechanism for accepting permission changes from extensions
$extended = $this -> extendedCan ( 'canView' , $member );
if ( $extended !== null ) {
return $extended ;
}
// admin override
2020-04-19 06:18:01 +02:00
if ( $member && Permission :: checkMember ( $member , [ " ADMIN " , " SITETREE_VIEW_ALL " ])) {
2017-01-25 21:59:25 +01:00
return true ;
}
// Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
if ( $this -> isOrphaned ()) {
return false ;
}
2017-05-12 02:47:46 +02:00
// Note: getInheritedPermissions() is disused in this instance
// to allow parent canView extensions to influence subpage canView()
2017-01-25 21:59:25 +01:00
// check for empty spec
2017-05-12 02:47:46 +02:00
if ( ! $this -> CanViewType || $this -> CanViewType === InheritedPermissions :: ANYONE ) {
2017-01-25 21:59:25 +01:00
return true ;
}
// check for inherit
2017-05-12 02:47:46 +02:00
if ( $this -> CanViewType === InheritedPermissions :: INHERIT ) {
2017-01-25 21:59:25 +01:00
if ( $this -> ParentID ) {
return $this -> Parent () -> canView ( $member );
} else {
return $this -> getSiteConfig () -> canViewPages ( $member );
}
}
// check for any logged-in users
2017-05-12 02:47:46 +02:00
if ( $this -> CanViewType === InheritedPermissions :: LOGGED_IN_USERS && $member && $member -> ID ) {
2017-01-25 21:59:25 +01:00
return true ;
}
// check for specific groups
2017-05-12 02:47:46 +02:00
if ( $this -> CanViewType === InheritedPermissions :: ONLY_THESE_USERS
2017-01-25 21:59:25 +01:00
&& $member
&& $member -> inGroups ( $this -> ViewerGroups ())
) {
return true ;
}
return false ;
}
/**
* Check if this page can be published
*
* @ param Member $member
* @ return bool
*/
public function canPublish ( $member = null )
{
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Check extension
$extended = $this -> extendedCan ( 'canPublish' , $member );
if ( $extended !== null ) {
return $extended ;
}
if ( Permission :: checkMember ( $member , " ADMIN " )) {
return true ;
}
// Default to relying on edit permission
return $this -> canEdit ( $member );
}
/**
* This function should return true if the current user can delete this page . It can be overloaded to customise the
* security model for an application .
*
* Denies permission if any of the following conditions is true :
* - canDelete () returns false on any extension
* - canEdit () returns false
* - any descendant page returns false for canDelete ()
*
* @ uses canDelete ()
* @ uses SiteTreeExtension -> canDelete ()
* @ uses canEdit ()
*
* @ param Member $member
* @ return bool True if the current user can delete this page
*/
public function canDelete ( $member = null )
{
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Standard mechanism for accepting permission changes from extensions
2017-05-12 02:47:46 +02:00
$extended = $this -> extendedCan ( 'canDelete' , $member );
2017-01-25 21:59:25 +01:00
if ( $extended !== null ) {
return $extended ;
}
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
return false ;
}
2017-01-25 21:59:25 +01:00
// Default permission check
2020-04-19 06:18:01 +02:00
if ( Permission :: checkMember ( $member , [ " ADMIN " , " SITETREE_EDIT_ALL " ])) {
2017-01-25 21:59:25 +01:00
return true ;
}
2017-05-12 02:47:46 +02:00
// Check inherited permissions
return static :: getPermissionChecker ()
-> canDelete ( $this -> ID , $member );
2017-01-25 21:59:25 +01:00
}
/**
* This function should return true if the current user can create new pages of this class , regardless of class . It
* can be overloaded to customise the security model for an application .
*
* By default , permission to create at the root level is based on the SiteConfig configuration , and permission to
* create beneath a parent is based on the ability to edit that parent page .
*
* Use { @ link canAddChildren ()} to control behaviour of creating children under this page .
*
* @ uses $can_create
* @ uses DataExtension -> canCreate ()
*
* @ param Member $member
2020-04-19 06:18:01 +02:00
* @ param array $context Optional array which may contain [ 'Parent' => $parentObj ]
2017-01-25 21:59:25 +01:00
* If a parent page is known , it will be checked for validity .
* If omitted , it will be assumed this is to be created as a top level page .
* @ return bool True if the current user can create pages on this class .
*/
2020-04-19 06:18:01 +02:00
public function canCreate ( $member = null , $context = [])
2017-01-25 21:59:25 +01:00
{
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Check parent (custom canCreate option for SiteTree)
// Block children not allowed for this parent type
$parent = isset ( $context [ 'Parent' ]) ? $context [ 'Parent' ] : null ;
2017-07-28 21:02:29 +02:00
$strictParentInstance = ( $parent && $parent instanceof SiteTree );
2022-04-13 07:07:59 +02:00
if ( $strictParentInstance && ! in_array ( static :: class , $parent -> allowedChildren () ? ? [])) {
2017-01-25 21:59:25 +01:00
return false ;
}
// Standard mechanism for accepting permission changes from extensions
$extended = $this -> extendedCan ( __FUNCTION__ , $member , $context );
if ( $extended !== null ) {
return $extended ;
}
// Check permission
if ( $member && Permission :: checkMember ( $member , " ADMIN " )) {
return true ;
}
// Fall over to inherited permissions
2017-07-28 21:02:29 +02:00
if ( $strictParentInstance && $parent -> exists ()) {
2017-01-25 21:59:25 +01:00
return $parent -> canAddChildren ( $member );
} else {
// This doesn't necessarily mean we are creating a root page, but that
// we don't know if there is a parent, so default to this permission
return SiteConfig :: current_site_config () -> canCreateTopLevel ( $member );
}
}
/**
* This function should return true if the current user can edit this page . It can be overloaded to customise the
* security model for an application .
*
* Denies permission if any of the following conditions is true :
* - canEdit () on any extension returns false
* - canView () return false
* - " CanEditType " directive is set to " Inherit " and any parent page return false for canEdit ()
* - " CanEditType " directive is set to " LoggedInUsers " and no user is logged in or doesn ' t have the
* CMS_Access_CMSMAIN permission code
* - " CanEditType " directive is set to " OnlyTheseUsers " and user is not in the given groups
*
* @ uses canView ()
* @ uses EditorGroups ()
* @ uses DataExtension -> canEdit ()
*
* @ param Member $member Set to false if you want to explicitly test permissions without a valid user ( useful for
* unit tests )
* @ return bool True if the current user can edit this page
*/
public function canEdit ( $member = null )
{
2017-05-12 02:47:46 +02:00
if ( ! $member ) {
2017-05-21 05:15:00 +02:00
$member = Security :: getCurrentUser ();
2017-01-25 21:59:25 +01:00
}
// Standard mechanism for accepting permission changes from extensions
2017-05-12 02:47:46 +02:00
$extended = $this -> extendedCan ( 'canEdit' , $member );
2017-01-25 21:59:25 +01:00
if ( $extended !== null ) {
return $extended ;
}
// Default permissions
2017-05-12 02:47:46 +02:00
if ( Permission :: checkMember ( $member , " SITETREE_EDIT_ALL " )) {
2017-01-25 21:59:25 +01:00
return true ;
}
2017-05-12 02:47:46 +02:00
// Check inherited permissions
return static :: getPermissionChecker ()
-> canEdit ( $this -> ID , $member );
2017-01-25 21:59:25 +01:00
}
/**
* Stub method to get the site config , unless the current class can provide an alternate .
*
* @ return SiteConfig
*/
public function getSiteConfig ()
{
$configs = $this -> invokeWithExtensions ( 'alternateSiteConfig' );
2022-04-13 07:07:59 +02:00
foreach ( array_filter ( $configs ? ? []) as $config ) {
2017-01-25 21:59:25 +01:00
return $config ;
}
return SiteConfig :: current_site_config ();
}
/**
2017-05-12 02:47:46 +02:00
* @ return PermissionChecker
2017-01-25 21:59:25 +01:00
*/
2017-05-12 02:47:46 +02:00
public static function getPermissionChecker ()
2017-01-25 21:59:25 +01:00
{
2017-05-12 02:47:46 +02:00
return Injector :: inst () -> get ( PermissionChecker :: class . '.sitetree' );
2017-01-25 21:59:25 +01:00
}
/**
* Collate selected descendants of this page .
*
* { @ link $condition } will be evaluated on each descendant , and if it is succeeds , that item will be added to the
* $collator array .
*
* @ param string $condition The PHP condition to be evaluated . The page will be called $item
* @ param array $collator An array , passed by reference , to collect all of the matching descendants .
* @ return bool
*/
public function collateDescendants ( $condition , & $collator )
{
2017-07-05 00:03:10 +02:00
// apply reasonable hierarchy limits
$threshold = Config :: inst () -> get ( Hierarchy :: class , 'node_threshold_leaf' );
if ( $this -> numChildren () > $threshold ) {
return false ;
}
2017-01-25 21:59:25 +01:00
$children = $this -> Children ();
if ( $children ) {
foreach ( $children as $item ) {
if ( eval ( " return $condition ; " )) {
$collator [] = $item ;
}
/** @var SiteTree $item */
$item -> collateDescendants ( $condition , $collator );
}
return true ;
}
return false ;
}
/**
2018-07-15 21:44:23 +02:00
* Return attributes for various meta tags , plus a title tag , in a keyed array .
* Array structure corresponds to arguments for HTML :: create_tag () . Example :
*
* $tags [ 'description' ] = [
* // html tag type, if omitted defaults to 'meta'
* 'tag' => 'meta' ,
* // attributes of html tag
* 'attributes' => [
* 'name' => 'description' ,
* 'content' => $this -> customMetaDescription (),
* ],
* // content of html tag. (True meta tags don't contain content)
* 'content' => null
* ];
*
* @ see HTML :: createTag ()
* @ return array
2017-01-25 21:59:25 +01:00
*/
2018-07-15 21:44:23 +02:00
public function MetaComponents ()
2017-01-25 21:59:25 +01:00
{
2018-07-15 21:44:23 +02:00
$tags = [];
$tags [ 'title' ] = [
'tag' => 'title' ,
'content' => $this -> obj ( 'Title' ) -> forTemplate ()
];
2017-01-25 21:59:25 +01:00
2022-03-09 02:48:02 +01:00
$generator = $this -> getGenerator ();
if ( $generator ) {
2018-07-15 21:44:23 +02:00
$tags [ 'generator' ] = [
'attributes' => [
'name' => 'generator' ,
2022-03-09 02:48:02 +01:00
'content' => $generator
]
2018-07-15 21:44:23 +02:00
];
2017-01-25 21:59:25 +01:00
}
2017-02-16 05:51:54 +01:00
$charset = ContentNegotiator :: config () -> uninherited ( 'encoding' );
2018-07-15 21:44:23 +02:00
$tags [ 'contentType' ] = [
'attributes' => [
'http-equiv' => 'Content-Type' ,
'content' => 'text/html; charset=' . $charset ,
],
];
2017-01-25 21:59:25 +01:00
if ( $this -> MetaDescription ) {
2018-07-15 21:44:23 +02:00
$tags [ 'description' ] = [
'attributes' => [
'name' => 'description' ,
'content' => $this -> MetaDescription ,
],
];
2017-01-25 21:59:25 +01:00
}
if ( Permission :: check ( 'CMS_ACCESS_CMSMain' )
&& $this -> ID > 0
) {
2018-07-15 21:44:23 +02:00
$tags [ 'pageId' ] = [
'attributes' => [
'name' => 'x-page-id' ,
'content' => $this -> ID ,
],
];
$tags [ 'cmsEditLink' ] = [
'attributes' => [
'name' => 'x-cms-edit-link' ,
'content' => $this -> CMSEditLink (),
],
];
}
$this -> extend ( 'MetaComponents' , $tags );
return $tags ;
}
2022-03-09 02:48:02 +01:00
/**
* Create the value for the meta generator tag
* Will suffix on the major . minor version of a stable tag
*
* @ return string
*/
private function getGenerator () : string
{
2022-04-13 07:07:59 +02:00
$generator = trim ( Config :: inst () -> get ( self :: class , 'meta_generator' ) ? ? '' );
2022-03-09 02:48:02 +01:00
if ( $generator === '' ) {
return '' ;
}
if ( self :: config () -> get ( 'show_meta_generator_version' )) {
$version = $this -> getVersionProvider () -> getModuleVersion ( 'silverstripe/framework' );
// Only include stable version numbers so as not to clutter any aggregate reports
// with non-standard versions e.g. forks
2022-04-13 07:07:59 +02:00
if ( preg_match ( '#^([0-9]+\.[0-9]+)\.[0-9]+$#' , $version ? ? '' , $m )) {
2022-03-09 02:48:02 +01:00
$generator .= ' ' . $m [ 1 ];
}
}
return $generator ;
}
/**
* @ return VersionProvider
*/
public function getVersionProvider () : VersionProvider
{
if ( $this -> versionProvider === null ) {
$this -> versionProvider = VersionProvider :: singleton ();
}
return $this -> versionProvider ;
}
/**
* @ param VersionProvider $versionProvider
*/
public function setVersionProvider ( VersionProvider $versionProvider ) : void
{
$this -> versionProvider = $versionProvider ;
}
2018-07-15 21:44:23 +02:00
/**
* Return the title , description , keywords and language metatags .
*
* @ param bool $includeTitle Show default < title >- tag , set to false for custom templating
* @ return string The XHTML metatags
*/
public function MetaTags ( $includeTitle = true )
{
$tags = [];
$tagsArray = $this -> MetaComponents ();
2022-04-13 07:07:59 +02:00
if ( ! $includeTitle || strtolower ( $includeTitle ? ? '' ) == 'false' ) {
2018-07-15 21:44:23 +02:00
unset ( $tagsArray [ 'title' ]);
}
foreach ( $tagsArray as $tagProps ) {
$tag = array_merge ([
'tag' => 'meta' ,
'attributes' => [],
'content' => null ,
], $tagProps );
$tags [] = HTML :: createTag ( $tag [ 'tag' ], $tag [ 'attributes' ], $tag [ 'content' ]);
2017-01-25 21:59:25 +01:00
}
2018-04-06 05:53:57 +02:00
$tagString = implode ( " \n " , $tags );
2017-01-25 21:59:25 +01:00
if ( $this -> ExtraMeta ) {
2018-04-06 05:53:57 +02:00
$tagString .= $this -> obj ( 'ExtraMeta' ) -> forTemplate ();
2017-01-25 21:59:25 +01:00
}
2018-04-06 05:53:57 +02:00
$this -> extend ( 'MetaTags' , $tagString );
2017-01-25 21:59:25 +01:00
2018-04-06 05:53:57 +02:00
return $tagString ;
2017-01-25 21:59:25 +01:00
}
/**
* Returns the object that contains the content that a user would associate with this page .
*
* Ordinarily , this is just the page itself , but for example on RedirectorPages or VirtualPages ContentSource () will
* return the page that is linked to .
*
* @ return $this
*/
public function ContentSource ()
{
return $this ;
}
/**
* Add default records to database .
*
* This function is called whenever the database is built , after the database tables have all been created . Overload
* this to add default records when the database is built , but make sure you call parent :: requireDefaultRecords () .
*/
public function requireDefaultRecords ()
{
parent :: requireDefaultRecords ();
// default pages
2018-04-06 05:53:57 +02:00
if ( static :: class === self :: class && $this -> config () -> get ( 'create_default_pages' )) {
$defaultHomepage = RootURLController :: config () -> get ( 'default_homepage_link' );
if ( ! SiteTree :: get_by_link ( $defaultHomepage )) {
2017-01-25 21:59:25 +01:00
$homepage = new Page ();
2017-05-08 07:57:24 +02:00
$homepage -> Title = _t ( __CLASS__ . '.DEFAULTHOMETITLE' , 'Home' );
$homepage -> Content = _t ( __CLASS__ . '.DEFAULTHOMECONTENT' , '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>.</p><p>You can now access the <a href="http://docs.silverstripe.org">developer documentation</a>, or begin the <a href="http://www.silverstripe.org/learn/lessons">SilverStripe lessons</a>.</p>' );
2018-04-06 05:53:57 +02:00
$homepage -> URLSegment = $defaultHomepage ;
2017-01-25 21:59:25 +01:00
$homepage -> Sort = 1 ;
$homepage -> write ();
$homepage -> copyVersionToStage ( Versioned :: DRAFT , Versioned :: LIVE );
$homepage -> flushCache ();
DB :: alteration_message ( 'Home page created' , 'created' );
}
2019-05-17 03:40:15 +02:00
$tablename = $this -> baseTable ();
if ( DB :: query ( " SELECT COUNT(*) FROM \" $tablename\ " " )->value() == 1) {
2017-01-25 21:59:25 +01:00
$aboutus = new Page ();
2017-05-08 07:57:24 +02:00
$aboutus -> Title = _t ( __CLASS__ . '.DEFAULTABOUTTITLE' , 'About Us' );
2017-01-25 21:59:25 +01:00
$aboutus -> Content = _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTABOUTCONTENT' ,
2017-01-25 21:59:25 +01:00
'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
);
$aboutus -> Sort = 2 ;
$aboutus -> write ();
$aboutus -> copyVersionToStage ( Versioned :: DRAFT , Versioned :: LIVE );
$aboutus -> flushCache ();
DB :: alteration_message ( 'About Us page created' , 'created' );
$contactus = new Page ();
2017-05-08 07:57:24 +02:00
$contactus -> Title = _t ( __CLASS__ . '.DEFAULTCONTACTTITLE' , 'Contact Us' );
2017-01-25 21:59:25 +01:00
$contactus -> Content = _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTCONTACTCONTENT' ,
2017-01-25 21:59:25 +01:00
'<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
);
$contactus -> Sort = 3 ;
$contactus -> write ();
$contactus -> copyVersionToStage ( Versioned :: DRAFT , Versioned :: LIVE );
$contactus -> flushCache ();
DB :: alteration_message ( 'Contact Us page created' , 'created' );
}
}
}
protected function onBeforeWrite ()
{
parent :: onBeforeWrite ();
// If Sort hasn't been set, make this page come after it's siblings
if ( ! $this -> Sort ) {
$parentID = ( $this -> ParentID ) ? $this -> ParentID : 0 ;
2019-05-17 03:40:15 +02:00
$tablename = $this -> baseTable ();
2017-01-25 21:59:25 +01:00
$this -> Sort = DB :: prepared_query (
2019-05-17 03:40:15 +02:00
" SELECT MAX( \" Sort \" ) + 1 FROM \" $tablename\ " WHERE \ " ParentID \" = ? " ,
2020-04-19 06:18:01 +02:00
[ $parentID ]
2017-01-25 21:59:25 +01:00
) -> value ();
}
// If there is no URLSegment set, generate one from Title
$defaultSegment = $this -> generateURLSegment ( _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE' ,
2017-01-25 21:59:25 +01:00
'New {pagetype}' ,
2020-04-19 06:18:01 +02:00
[ 'pagetype' => $this -> i18n_singular_name ()]
2017-01-25 21:59:25 +01:00
));
if (( ! $this -> URLSegment || $this -> URLSegment == $defaultSegment ) && $this -> Title ) {
$this -> URLSegment = $this -> generateURLSegment ( $this -> Title );
} elseif ( $this -> isChanged ( 'URLSegment' , 2 )) {
// Do a strict check on change level, to avoid double encoding caused by
// bogus changes through forceChange()
$filter = URLSegmentFilter :: create ();
$this -> URLSegment = $filter -> filter ( $this -> URLSegment );
// If after sanitising there is no URLSegment, give it a reasonable default
if ( ! $this -> URLSegment ) {
$this -> URLSegment = " page- $this->ID " ;
}
}
2019-07-16 05:16:09 +02:00
2019-04-15 04:33:15 +02:00
// need to set the default values of a page e.g."Untitled [Page type]"
if ( empty ( $this -> Title )) {
$this -> Title = _t (
'SilverStripe\\CMS\\Model\\SiteTree.UNTITLED' ,
'Untitled {pagetype}' ,
[ 'pagetype' => $this -> i18n_singular_name ()]
);
};
2017-01-25 21:59:25 +01:00
// Ensure that this object has a non-conflicting URLSegment value.
$count = 2 ;
while ( ! $this -> validURLSegment ()) {
2022-04-13 07:07:59 +02:00
$this -> URLSegment = preg_replace ( '/-[0-9]+$/' , '' , $this -> URLSegment ? ? '' ) . '-' . $count ;
2017-01-25 21:59:25 +01:00
$count ++ ;
}
// Check to see if we've only altered fields that shouldn't affect versioning
2020-04-19 06:18:01 +02:00
$fieldsIgnoredByVersioning = [ 'HasBrokenLink' , 'Status' , 'HasBrokenFile' , 'ToDo' , 'VersionID' , 'SaveCount' ];
2022-04-13 07:07:59 +02:00
$changedFields = array_keys ( $this -> getChangedFields ( true , 2 ) ? ? []);
2017-01-25 21:59:25 +01:00
// This more rigorous check is inline with the test that write() does to decide whether or not to write to the
// DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
2022-04-13 07:07:59 +02:00
$oneChangedFields = array_keys ( $this -> getChangedFields ( true , 1 ) ? ? []);
2017-01-25 21:59:25 +01:00
2022-04-13 07:07:59 +02:00
if ( $oneChangedFields && ! array_diff ( $changedFields ? ? [], $fieldsIgnoredByVersioning )) {
2018-03-14 04:34:46 +01:00
$this -> setNextWriteWithoutVersion ( true );
2017-01-25 21:59:25 +01:00
}
2020-08-26 00:13:39 +02:00
2022-08-23 06:44:58 +02:00
$this -> sanitiseExtraMeta ();
2020-08-26 00:13:39 +02:00
// Flush cached [embed] shortcodes
// Flush on both DRAFT and LIVE because VersionedCacheAdapter has separate caches for both
// Clear both caches at once for the scenario where a CMS-author updates a remote resource
// on a 3rd party service and the url for the resource stays the same. Either saving or publishing
// the page will clear both caches. This allow a CMS-author to clear the live cache by only
// saving the draft page and not publishing any changes they may not want live yet.
$parser = ShortcodeParser :: get ( 'default' );
foreach ([ Versioned :: DRAFT , Versioned :: LIVE ] as $stage ) {
Versioned :: withVersionedMode ( function () use ( $parser , $stage ) {
Versioned :: set_reading_mode ( " Stage. $stage " );
// $this->Content may be null on brand new SiteTree objects
if ( ! $this -> Content ) {
return ;
}
EmbedShortcodeProvider :: flushCachedShortcodes ( $parser , $this -> Content );
});
}
2017-01-25 21:59:25 +01:00
}
2022-08-23 06:44:58 +02:00
private function sanitiseExtraMeta () : void
{
$htmlValue = HTMLValue :: create ( $this -> ExtraMeta );
/** @var DOMElement $el */
foreach ( $htmlValue -> query ( '//*' ) as $el ) {
/** @var DOMAttr $attr */
$attributes = $el -> attributes ;
for ( $i = count ( $attributes ) - 1 ; $i >= 0 ; $i -- ) {
$attr = $attributes -> item ( $i );
// remove any attribute starting with 'on' e.g. onclick
// and remove the accesskey attribute
if ( substr ( $attr -> name , 0 , 2 ) === 'on' ||
$attr -> name === 'accesskey'
) {
$el -> removeAttributeNode ( $attr );
}
}
}
$this -> ExtraMeta = $htmlValue -> getContent ();
}
2017-01-25 21:59:25 +01:00
/**
* Trigger synchronisation of link tracking
*
* { @ see SiteTreeLinkTracking :: augmentSyncLinkTracking }
*/
public function syncLinkTracking ()
{
$this -> extend ( 'augmentSyncLinkTracking' );
}
public function onBeforeDelete ()
{
parent :: onBeforeDelete ();
// If deleting this page, delete all its children.
2018-04-06 05:53:57 +02:00
if ( $this -> isInDB () && SiteTree :: config () -> get ( 'enforce_strict_hierarchy' )) {
foreach ( $this -> AllChildren () as $child ) {
2017-01-25 21:59:25 +01:00
/** @var SiteTree $child */
$child -> delete ();
}
}
}
public function onAfterDelete ()
{
2017-06-21 06:29:40 +02:00
$this -> updateDependentPages ();
2017-01-25 21:59:25 +01:00
parent :: onAfterDelete ();
}
public function flushCache ( $persistent = true )
{
parent :: flushCache ( $persistent );
$this -> _cache_statusFlags = null ;
}
2017-11-30 03:56:16 +01:00
/**
* Flushes the member specific cache for creatable children
2017-12-13 03:36:35 +01:00
*
* @ param array $memberIDs
2017-11-30 03:56:16 +01:00
*/
public function flushMemberCache ( $memberIDs = null )
{
2017-12-13 03:36:35 +01:00
$cache = SiteTree :: singleton () -> getCreatableChildrenCache ();
2017-11-30 03:56:16 +01:00
2017-12-13 03:36:35 +01:00
if ( ! $memberIDs ) {
2017-11-30 03:56:16 +01:00
$cache -> clear ();
2017-12-13 03:36:35 +01:00
return ;
}
foreach ( $memberIDs as $memberID ) {
$key = $this -> generateChildrenCacheKey ( $memberID );
$cache -> delete ( $key );
2017-11-30 03:56:16 +01:00
}
}
2017-01-25 21:59:25 +01:00
public function validate ()
{
$result = parent :: validate ();
// Allowed children validation
$parent = $this -> getParent ();
if ( $parent && $parent -> exists ()) {
// No need to check for subclasses or instanceof, as allowedChildren() already
// deconstructs any inheritance trees already.
$allowed = $parent -> allowedChildren ();
$subject = ( $this instanceof VirtualPage && $this -> CopyContentFromID )
? $this -> CopyContentFrom ()
: $this ;
2022-04-13 07:07:59 +02:00
if ( ! in_array ( $subject -> ClassName , $allowed ? ? [])) {
2017-01-25 21:59:25 +01:00
$result -> addError (
_t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.PageTypeNotAllowed' ,
2017-01-25 21:59:25 +01:00
'Page type "{type}" not allowed as child of this parent page' ,
2020-04-19 06:18:01 +02:00
[ 'type' => $subject -> i18n_singular_name ()]
2017-01-25 21:59:25 +01:00
),
ValidationResult :: TYPE_ERROR ,
'ALLOWED_CHILDREN'
);
}
}
// "Can be root" validation
2017-08-22 23:46:46 +02:00
if ( ! $this -> config () -> get ( 'can_be_root' ) && ! $this -> ParentID ) {
2017-01-25 21:59:25 +01:00
$result -> addError (
_t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.PageTypNotAllowedOnRoot' ,
2017-01-25 21:59:25 +01:00
'Page type "{type}" is not allowed on the root level' ,
2020-04-19 06:18:01 +02:00
[ 'type' => $this -> i18n_singular_name ()]
2017-01-25 21:59:25 +01:00
),
ValidationResult :: TYPE_ERROR ,
'CAN_BE_ROOT'
);
}
2022-08-23 06:44:58 +02:00
// Ensure ExtraMeta can be turned into valid HTML
if ( $this -> ExtraMeta && ! HTMLValue :: create ( $this -> ExtraMeta ) -> getContent ()) {
$result -> addError (
_t (
'SilverStripe\\CMS\\Model\\SiteTree.InvalidExtraMeta' ,
'Custom Meta Tags does not contain valid HTML' ,
)
);
}
2017-01-25 21:59:25 +01:00
return $result ;
}
/**
* Returns true if this object has a URLSegment value that does not conflict with any other objects . This method
* checks for :
* - A page with the same URLSegment that has a conflict
* - Conflicts with actions on the parent page
* - A conflict caused by a root page having the same URLSegment as a class name
*
* @ return bool
*/
public function validURLSegment ()
{
2018-01-24 05:29:10 +01:00
// Check known urlsegment blacklists
2018-04-06 05:53:57 +02:00
if ( self :: config () -> get ( 'nested_urls' ) && $this -> ParentID ) {
2018-01-24 05:29:10 +01:00
// Guard against url segments for sub-pages
$parent = $this -> Parent ();
2017-01-25 21:59:25 +01:00
if ( $controller = ModelAsController :: controller_for ( $parent )) {
if ( $controller instanceof Controller && $controller -> hasAction ( $this -> URLSegment )) {
return false ;
}
}
2022-04-13 07:07:59 +02:00
} elseif ( in_array ( strtolower ( $this -> URLSegment ? ? '' ), $this -> getExcludedURLSegments () ? ? [])) {
2018-01-24 05:29:10 +01:00
// Guard against url segments for the base page
// Default to '-2', onBeforeWrite takes care of further possible clashes
return false ;
2017-01-25 21:59:25 +01:00
}
// If any of the extensions return `0` consider the segment invalid
$extensionResponses = array_filter (
( array ) $this -> extend ( 'augmentValidURLSegment' ),
function ( $response ) {
return ! is_null ( $response );
}
);
if ( $extensionResponses ) {
return min ( $extensionResponses );
}
2018-01-24 05:29:10 +01:00
// Check for clashing pages by url, id, and parent
$source = SiteTree :: get () -> filter ( 'URLSegment' , $this -> URLSegment );
if ( $this -> ID ) {
$source = $source -> exclude ( 'ID' , $this -> ID );
}
2018-04-06 05:53:57 +02:00
if ( self :: config () -> get ( 'nested_urls' )) {
2018-01-24 05:29:10 +01:00
$source = $source -> filter ( 'ParentID' , $this -> ParentID ? $this -> ParentID : 0 );
}
return ! $source -> exists ();
2017-01-25 21:59:25 +01:00
}
/**
* Generate a URL segment based on the title provided .
*
* If { @ link Extension } s wish to alter URL segment generation , they can do so by defining
* updateURLSegment ( & $url , $title ) . $url will be passed by reference and should be modified . $title will contain
* the title that was originally used as the source of this generated URL . This lets extensions either start from
* scratch , or incrementally modify the generated URL .
*
* @ param string $title Page title
* @ return string Generated url segment
*/
public function generateURLSegment ( $title )
{
$filter = URLSegmentFilter :: create ();
2017-07-09 07:42:36 +02:00
$filteredTitle = $filter -> filter ( $title );
2017-01-25 21:59:25 +01:00
// Fallback to generic page name if path is empty (= no valid, convertable characters)
2017-07-09 07:42:36 +02:00
if ( ! $filteredTitle || $filteredTitle == '-' || $filteredTitle == '-1' ) {
$filteredTitle = " page- $this->ID " ;
2017-01-25 21:59:25 +01:00
}
// Hook for extensions
2017-07-09 07:42:36 +02:00
$this -> extend ( 'updateURLSegment' , $filteredTitle , $title );
2017-01-25 21:59:25 +01:00
2017-07-09 07:42:36 +02:00
return $filteredTitle ;
2017-01-25 21:59:25 +01:00
}
/**
* Gets the URL segment for the latest draft version of this page .
*
* @ return string
*/
public function getStageURLSegment ()
{
2019-05-17 03:40:15 +02:00
$tablename = $this -> baseTable ();
2018-04-06 05:53:57 +02:00
/** @var SiteTree $stageRecord */
$stageRecord = Versioned :: get_one_by_stage ( self :: class , Versioned :: DRAFT , [
2019-05-17 03:40:15 +02:00
" \" $tablename\ " . \ " ID \" " => $this -> ID
2018-04-06 05:53:57 +02:00
]);
2017-01-25 21:59:25 +01:00
return ( $stageRecord ) ? $stageRecord -> URLSegment : null ;
}
/**
* Gets the URL segment for the currently published version of this page .
*
* @ return string
*/
public function getLiveURLSegment ()
{
2019-05-17 03:40:15 +02:00
$tablename = $this -> baseTable ();
2018-04-06 05:53:57 +02:00
/** @var SiteTree $liveRecord */
$liveRecord = Versioned :: get_one_by_stage ( self :: class , Versioned :: LIVE , [
2019-05-17 03:40:15 +02:00
" \" $tablename\ " . \ " ID \" " => $this -> ID
2018-04-06 05:53:57 +02:00
]);
2017-01-25 21:59:25 +01:00
return ( $liveRecord ) ? $liveRecord -> URLSegment : null ;
}
2018-04-06 05:53:57 +02:00
/**
* Get the back - link tracking objects that link to this page
*
2018-11-14 06:01:29 +01:00
* @ return ArrayList | DataObject []
2018-04-06 05:53:57 +02:00
*/
public function BackLinkTracking ()
{
// @todo - Implement PolymorphicManyManyList to replace this
$list = ArrayList :: create ();
2018-11-14 06:01:29 +01:00
2018-11-15 03:57:17 +01:00
$siteTreelinkTable = SiteTreeLink :: singleton () -> baseTable ();
2018-11-14 06:01:29 +01:00
// Get the list of back links classes
2018-11-15 03:57:17 +01:00
$parentClasses = $this -> BackLinks () -> exclude ([ 'ParentClass' => null ]) -> columnUnique ( 'ParentClass' );
2018-11-14 06:01:29 +01:00
// Get list of sitreTreelink and join them to the their parent class to make sure we don't get orphan records.
2018-11-15 03:57:17 +01:00
foreach ( $parentClasses as $parentClass ) {
$joinClause = sprintf (
" \" %s \" . \" ParentID \" = \" %s \" . \" ID \" " ,
$siteTreelinkTable ,
DataObject :: singleton ( $parentClass ) -> baseTable ()
);
$links = DataObject :: get ( $parentClass )
2018-11-14 06:01:29 +01:00
-> innerJoin (
2018-11-15 03:57:17 +01:00
$siteTreelinkTable ,
$joinClause
2018-11-14 06:01:29 +01:00
)
2018-11-15 03:57:17 +01:00
-> where ([
" \" $siteTreelinkTable\ " . \ " LinkedID \" " => $this -> ID ,
" \" $siteTreelinkTable\ " . \ " ParentClass \" " => $parentClass ,
])
2018-11-14 06:01:29 +01:00
-> alterDataQuery ( function ( $query ) {
$query -> selectField ( " 'Content link' " , " DependentLinkType " );
2018-11-15 03:57:17 +01:00
});
2018-11-14 06:01:29 +01:00
$list -> merge ( $links );
2018-04-06 05:53:57 +02:00
}
2018-11-14 06:01:29 +01:00
2018-04-06 05:53:57 +02:00
return $list ;
}
2017-01-25 21:59:25 +01:00
/**
* Returns the pages that depend on this page . This includes virtual pages , pages that link to it , etc .
*
* @ param bool $includeVirtuals Set to false to exlcude virtual pages .
2018-04-06 05:53:57 +02:00
* @ return ArrayList | SiteTree []
2017-01-25 21:59:25 +01:00
*/
public function DependentPages ( $includeVirtuals = true )
{
2018-09-27 14:07:42 +02:00
if ( class_exists ( Subsite :: class )) {
2017-01-25 21:59:25 +01:00
$origDisableSubsiteFilter = Subsite :: $disable_subsite_filter ;
Subsite :: disable_subsite_filter ( true );
}
// Content links
2018-11-28 13:44:52 +01:00
$items = ArrayList :: create ();
// If the record hasn't been written yet, it cannot be depended on yet
if ( ! $this -> isInDB ()) {
return $items ;
}
2017-01-25 21:59:25 +01:00
// We merge all into a regular SS_List, because DataList doesn't support merge
if ( $contentLinks = $this -> BackLinkTracking ()) {
2018-11-14 06:01:29 +01:00
$items -> merge ( $contentLinks );
2017-01-25 21:59:25 +01:00
}
// Virtual pages
if ( $includeVirtuals ) {
2018-11-14 06:01:29 +01:00
$virtuals = $this -> VirtualPages ()
-> alterDataQuery ( function ( $query ) {
$query -> selectField ( " 'Virtual page' " , " DependentLinkType " );
});
$items -> merge ( $virtuals );
2017-01-25 21:59:25 +01:00
}
// Redirector pages
2020-04-19 06:18:01 +02:00
$redirectors = RedirectorPage :: get () -> where ([
2018-11-14 06:01:29 +01:00
'"RedirectorPage"."RedirectionType"' => 'Internal' ,
'"RedirectorPage"."LinkToID"' => $this -> ID
2020-04-19 06:18:01 +02:00
]) -> alterDataQuery ( function ( $query ) {
2018-11-14 06:01:29 +01:00
$query -> selectField ( " 'Redirector page' " , " DependentLinkType " );
});
$items -> merge ( $redirectors );
2017-01-25 21:59:25 +01:00
2018-09-27 14:07:42 +02:00
if ( class_exists ( Subsite :: class )) {
2017-01-25 21:59:25 +01:00
Subsite :: disable_subsite_filter ( $origDisableSubsiteFilter );
}
return $items ;
}
/**
* Return all virtual pages that link to this page .
*
* @ return DataList
*/
public function VirtualPages ()
{
$pages = parent :: VirtualPages ();
// Disable subsite filter for these pages
if ( $pages instanceof DataList ) {
return $pages -> setDataQueryParam ( 'Subsite.filter' , false );
}
2018-09-27 14:07:42 +02:00
return $pages ;
2017-01-25 21:59:25 +01:00
}
/**
* Returns a FieldList with which to create the main editing form .
*
* You can override this in your child classes to add extra fields - first get the parent fields using
* parent :: getCMSFields (), then use addFieldToTab () on the FieldList .
*
* See { @ link getSettingsFields ()} for a different set of fields concerned with configuration aspects on the record ,
* e . g . access control .
*
* @ return FieldList The fields to be displayed in the CMS
*/
public function getCMSFields ()
{
$dependentNote = '' ;
$dependentTable = new LiteralField ( 'DependentNote' , '<p></p>' );
// Create a table for showing pages linked to this one
$dependentPages = $this -> DependentPages ();
$dependentPagesCount = $dependentPages -> count ();
if ( $dependentPagesCount ) {
2020-04-19 06:18:01 +02:00
$dependentColumns = [
2017-01-25 21:59:25 +01:00
'Title' => $this -> fieldLabel ( 'Title' ),
2017-05-08 07:57:24 +02:00
'DependentLinkType' => _t ( __CLASS__ . '.DependtPageColumnLinkType' , 'Link type' ),
2020-04-19 06:18:01 +02:00
];
2018-09-27 14:07:42 +02:00
if ( class_exists ( Subsite :: class )) {
$dependentColumns [ 'Subsite.Title' ] = Subsite :: singleton () -> i18n_singular_name ();
2017-01-25 21:59:25 +01:00
}
2017-05-08 07:57:24 +02:00
$dependentNote = new LiteralField ( 'DependentNote' , '<p>' . _t ( __CLASS__ . '.DEPENDENT_NOTE' , 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.' ) . '</p>' );
2017-01-25 21:59:25 +01:00
$dependentTable = GridField :: create (
'DependentPages' ,
false ,
$dependentPages
);
/** @var GridFieldDataColumns $dataColumns */
2018-10-09 03:16:43 +02:00
$dataColumns = $dependentTable -> getConfig () -> getComponentByType ( GridFieldDataColumns :: class );
2017-01-25 21:59:25 +01:00
$dataColumns
-> setDisplayFields ( $dependentColumns )
2020-04-19 06:18:01 +02:00
-> setFieldFormatting ([
2017-01-25 21:59:25 +01:00
'Title' => function ( $value , & $item ) {
2019-07-16 05:16:09 +02:00
$title = $item -> Title ;
$untitled = _t (
__CLASS__ . '.UntitledDependentObject' ,
'Untitled {instanceType}' ,
[ 'instanceType' => $item -> i18n_singular_name ()]
);
$tag = $item -> hasMethod ( 'CMSEditLink' ) ? 'a' : 'span' ;
2017-01-25 21:59:25 +01:00
return sprintf (
2019-07-16 05:16:09 +02:00
'<%s%s class="dependent-content__edit-link %s">%s</%s>' ,
$tag ,
$tag === 'a' ? sprintf ( ' href="%s"' , $item -> CMSEditLink ()) : '' ,
$title ? '' : 'dependent-content__edit-link--untitled' ,
$title ? Convert :: raw2xml ( $title ) : $untitled ,
$tag
2017-01-25 21:59:25 +01:00
);
}
2020-04-19 06:18:01 +02:00
]);
2022-02-12 06:48:17 +01:00
$dependentTable -> getConfig () -> addComponent ( Injector :: inst () -> create ( GridFieldLazyLoader :: class ));
2017-01-25 21:59:25 +01:00
}
$baseLink = Controller :: join_links (
Director :: absoluteBaseURL (),
2018-04-06 05:53:57 +02:00
( self :: config () -> get ( 'nested_urls' ) && $this -> ParentID ? $this -> Parent () -> RelativeLink ( true ) : null )
2017-01-25 21:59:25 +01:00
);
$urlsegment = SiteTreeURLSegmentField :: create ( " URLSegment " , $this -> fieldLabel ( 'URLSegment' ))
-> setURLPrefix ( $baseLink )
2020-08-06 04:23:58 +02:00
-> setURLSuffix ( '?stage=Stage' )
2017-01-25 21:59:25 +01:00
-> setDefaultURL ( $this -> generateURLSegment ( _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE' ,
2017-01-25 21:59:25 +01:00
'New {pagetype}' ,
2020-04-19 06:18:01 +02:00
[ 'pagetype' => $this -> i18n_singular_name ()]
2019-08-16 05:25:21 +02:00
)))
-> addExtraClass (( $this -> isHomePage () ? 'homepage-warning' : '' ));
2018-04-06 05:53:57 +02:00
$helpText = ( self :: config () -> get ( 'nested_urls' ) && $this -> numChildren ())
2017-01-25 21:59:25 +01:00
? $this -> fieldLabel ( 'LinkChangeNote' )
: '' ;
2019-02-04 10:04:48 +01:00
if ( ! URLSegmentFilter :: create () -> getAllowMultibyte ()) {
2017-04-20 03:15:29 +02:00
$helpText .= _t ( 'SilverStripe\\CMS\\Forms\\SiteTreeURLSegmentField.HelpChars' , ' Special characters are automatically converted or removed.' );
2017-01-25 21:59:25 +01:00
}
$urlsegment -> setHelpText ( $helpText );
$fields = new FieldList (
$rootTab = new TabSet (
" Root " ,
$tabMain = new Tab (
'Main' ,
new TextField ( " Title " , $this -> fieldLabel ( 'Title' )),
$urlsegment ,
new TextField ( " MenuTitle " , $this -> fieldLabel ( 'MenuTitle' )),
2017-12-07 03:36:51 +01:00
$htmlField = HTMLEditorField :: create ( " Content " , _t ( __CLASS__ . '.HTMLEDITORTITLE' , " Content " , 'HTML editor title' )),
2017-01-25 21:59:25 +01:00
ToggleCompositeField :: create (
'Metadata' ,
2017-05-08 07:57:24 +02:00
_t ( __CLASS__ . '.MetadataToggle' , 'Metadata' ),
2020-04-19 06:18:01 +02:00
[
2017-01-25 21:59:25 +01:00
$metaFieldDesc = new TextareaField ( " MetaDescription " , $this -> fieldLabel ( 'MetaDescription' )),
$metaFieldExtra = new TextareaField ( " ExtraMeta " , $this -> fieldLabel ( 'ExtraMeta' ))
2020-04-19 06:18:01 +02:00
]
2017-01-25 21:59:25 +01:00
) -> setHeadingLevel ( 4 )
),
$tabDependent = new Tab (
'Dependent' ,
$dependentNote ,
$dependentTable
)
)
);
$htmlField -> addExtraClass ( 'stacked' );
// Help text for MetaData on page content editor
$metaFieldDesc
-> setRightTitle (
_t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.METADESCHELP' ,
2017-01-25 21:59:25 +01:00
" Search engines use this content for displaying search results (although it will not influence their ranking). "
)
)
-> addExtraClass ( 'help' );
$metaFieldExtra
-> setRightTitle (
_t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.METAEXTRAHELP' ,
2020-10-12 22:38:03 +02:00
" HTML tags for additional meta information. For example <meta name= \" customName \" content= \" your custom content here \" > "
2017-01-25 21:59:25 +01:00
)
)
-> addExtraClass ( 'help' );
// Conditional dependent pages tab
if ( $dependentPagesCount ) {
2017-05-08 07:57:24 +02:00
$tabDependent -> setTitle ( _t ( __CLASS__ . '.TABDEPENDENT' , " Dependent pages " ) . " ( $dependentPagesCount ) " );
2017-01-25 21:59:25 +01:00
} else {
$fields -> removeFieldFromTab ( 'Root' , 'Dependent' );
}
2019-08-14 18:38:36 +02:00
$tabMain -> setTitle ( _t ( __CLASS__ . '.TABCONTENT' , " Main content " ));
2017-01-25 21:59:25 +01:00
if ( $this -> ObsoleteClassName ) {
$obsoleteWarning = _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.OBSOLETECLASS' ,
2017-01-25 21:59:25 +01:00
" This page is of obsolete type { type}. Saving will reset its type and you may lose data " ,
2020-04-19 06:18:01 +02:00
[ 'type' => $this -> ObsoleteClassName ]
2017-01-25 21:59:25 +01:00
);
$fields -> addFieldToTab (
" Root.Main " ,
2020-09-16 01:05:55 +02:00
LiteralField :: create ( " ObsoleteWarningHeader " , " <p class= \" alert alert-warning \" > $obsoleteWarning </p> " ),
2017-01-25 21:59:25 +01:00
" Title "
);
}
2019-08-20 11:45:29 +02:00
if ( file_exists ( PUBLIC_PATH . '/install.php' )) {
2018-02-02 01:02:47 +01:00
$fields -> addFieldToTab ( 'Root.Main' , LiteralField :: create (
'InstallWarningHeader' ,
'<div class="alert alert-warning">' . _t (
__CLASS__ . '.REMOVE_INSTALL_WARNING' ,
2017-01-25 21:59:25 +01:00
" Warning: You should remove install.php from this SilverStripe install for security reasons. "
)
2018-02-02 01:02:47 +01:00
. '</div>'
), 'Title' );
2017-01-25 21:59:25 +01:00
}
if ( self :: $runCMSFieldsExtensions ) {
$this -> extend ( 'updateCMSFields' , $fields );
}
return $fields ;
}
/**
* Returns fields related to configuration aspects on this record , e . g . access control . See { @ link getCMSFields ()}
* for content - related fields .
*
* @ return FieldList
*/
public function getSettingsFields ()
{
2017-06-01 11:37:48 +02:00
$mapFn = function ( $groups = []) {
$map = [];
foreach ( $groups as $group ) {
// Listboxfield values are escaped, use ASCII char instead of »
$map [ $group -> ID ] = $group -> getBreadcrumbs ( ' > ' );
}
asort ( $map );
return $map ;
};
$viewAllGroupsMap = $mapFn ( Permission :: get_groups_by_permission ([ 'SITETREE_VIEW_ALL' , 'ADMIN' ]));
$editAllGroupsMap = $mapFn ( Permission :: get_groups_by_permission ([ 'SITETREE_EDIT_ALL' , 'ADMIN' ]));
2017-01-25 21:59:25 +01:00
$fields = new FieldList (
$rootTab = new TabSet (
" Root " ,
$tabBehaviour = new Tab (
'Settings' ,
new DropdownField (
" ClassName " ,
$this -> fieldLabel ( 'ClassName' ),
$this -> getClassDropdown ()
),
2020-05-01 05:45:08 +02:00
$parentTypeSelector = ( new CompositeField (
2020-04-19 06:18:01 +02:00
$parentType = new OptionsetField ( " ParentType " , _t ( " SilverStripe \\ CMS \\ Model \\ SiteTree.PAGELOCATION " , " Page location " ), [
2017-04-20 03:15:29 +02:00
" root " => _t ( " SilverStripe \\ CMS \\ Model \\ SiteTree.PARENTTYPE_ROOT " , " Top-level page " ),
" subpage " => _t ( " SilverStripe \\ CMS \\ Model \\ SiteTree.PARENTTYPE_SUBPAGE " , " Sub-page underneath a parent page " ),
2020-04-19 06:18:01 +02:00
]),
2017-01-25 21:59:25 +01:00
$parentIDField = new TreeDropdownField ( " ParentID " , $this -> fieldLabel ( 'ParentID' ), self :: class , 'ID' , 'MenuTitle' )
2020-05-01 05:45:08 +02:00
)) -> setTitle ( _t ( " SilverStripe \\ CMS \\ Model \\ SiteTree.PAGELOCATION " , " Page location " )),
2017-01-25 21:59:25 +01:00
$visibility = new FieldGroup (
new CheckboxField ( " ShowInMenus " , $this -> fieldLabel ( 'ShowInMenus' )),
new CheckboxField ( " ShowInSearch " , $this -> fieldLabel ( 'ShowInSearch' ))
),
$viewersOptionsField = new OptionsetField (
" CanViewType " ,
2017-05-08 07:57:24 +02:00
_t ( __CLASS__ . '.ACCESSHEADER' , " Who can view this page? " )
2017-01-25 21:59:25 +01:00
),
2017-08-30 12:40:56 +02:00
$viewerGroupsField = TreeMultiselectField :: create (
" ViewerGroups " ,
_t ( __CLASS__ . '.VIEWERGROUPS' , " Viewer Groups " ),
Group :: class
),
2017-01-25 21:59:25 +01:00
$editorsOptionsField = new OptionsetField (
" CanEditType " ,
2017-05-08 07:57:24 +02:00
_t ( __CLASS__ . '.EDITHEADER' , " Who can edit this page? " )
2017-01-25 21:59:25 +01:00
),
2017-08-30 12:40:56 +02:00
$editorGroupsField = TreeMultiselectField :: create (
" EditorGroups " ,
_t ( __CLASS__ . '.EDITORGROUPS' , " Editor Groups " ),
Group :: class
)
2017-01-25 21:59:25 +01:00
)
)
);
$parentType -> addExtraClass ( 'noborder' );
2022-01-25 01:49:11 +01:00
$visibility -> setName ( 'Visibility' ) -> setTitle ( $this -> fieldLabel ( 'Visibility' ));
2017-01-25 21:59:25 +01:00
// This filter ensures that the ParentID dropdown selection does not show this node,
// or its descendents, as this causes vanishing bugs
2017-05-19 05:57:53 +02:00
$parentIDField -> setFilterFunction ( function ( $node ) {
return $node -> ID != $this -> ID ;
});
2017-01-25 21:59:25 +01:00
$parentTypeSelector -> addExtraClass ( 'parentTypeSelector' );
2017-05-08 07:57:24 +02:00
$tabBehaviour -> setTitle ( _t ( __CLASS__ . '.TABBEHAVIOUR' , " Behavior " ));
2017-01-25 21:59:25 +01:00
// Make page location fields read-only if the user doesn't have the appropriate permission
if ( ! Permission :: check ( " SITETREE_REORGANISE " )) {
$fields -> makeFieldReadonly ( 'ParentType' );
if ( $this -> getParentType () === 'root' ) {
$fields -> removeByName ( 'ParentID' );
} else {
$fields -> makeFieldReadonly ( 'ParentID' );
}
}
2018-07-04 00:41:50 +02:00
$inheritMessage = $this -> ParentID !== 0 ?
_t ( __CLASS__ . '.INHERIT' , " Inherit from parent page " ) :
_t ( __CLASS__ . '.INHERITSITECONFIG' , " Inherit from site access settings " );
2018-07-02 05:42:24 +02:00
2017-05-12 02:47:46 +02:00
$viewersOptionsSource = [
2018-07-02 05:42:24 +02:00
InheritedPermissions :: INHERIT => $inheritMessage ,
2017-05-12 02:47:46 +02:00
InheritedPermissions :: ANYONE => _t ( __CLASS__ . '.ACCESSANYONE' , " Anyone " ),
InheritedPermissions :: LOGGED_IN_USERS => _t ( __CLASS__ . '.ACCESSLOGGEDIN' , " Logged-in users " ),
InheritedPermissions :: ONLY_THESE_USERS => _t (
__CLASS__ . '.ACCESSONLYTHESE' ,
2017-07-03 11:37:15 +02:00
" Only these groups (choose from list) "
2017-05-12 02:47:46 +02:00
),
];
2017-01-25 21:59:25 +01:00
$viewersOptionsField -> setSource ( $viewersOptionsSource );
2017-05-12 02:47:46 +02:00
// Editors have same options, except no "Anyone"
$editorsOptionsSource = $viewersOptionsSource ;
unset ( $editorsOptionsSource [ InheritedPermissions :: ANYONE ]);
2017-01-25 21:59:25 +01:00
$editorsOptionsField -> setSource ( $editorsOptionsSource );
2017-06-01 11:37:48 +02:00
if ( $viewAllGroupsMap ) {
$viewerGroupsField -> setDescription ( _t (
'SilverStripe\\CMS\\Model\\SiteTree.VIEWER_GROUPS_FIELD_DESC' ,
'Groups with global view permissions: {groupList}' ,
2022-04-13 07:07:59 +02:00
[ 'groupList' => implode ( ', ' , array_values ( $viewAllGroupsMap ? ? []))]
2017-06-01 11:37:48 +02:00
));
}
if ( $editAllGroupsMap ) {
$editorGroupsField -> setDescription ( _t (
'SilverStripe\\CMS\\Model\\SiteTree.EDITOR_GROUPS_FIELD_DESC' ,
'Groups with global edit permissions: {groupList}' ,
2022-04-13 07:07:59 +02:00
[ 'groupList' => implode ( ', ' , array_values ( $editAllGroupsMap ? ? []))]
2017-06-01 11:37:48 +02:00
));
}
2017-01-25 21:59:25 +01:00
if ( ! Permission :: check ( 'SITETREE_GRANT_ACCESS' )) {
$fields -> makeFieldReadonly ( $viewersOptionsField );
2017-05-12 02:47:46 +02:00
if ( $this -> CanEditType === InheritedPermissions :: ONLY_THESE_USERS ) {
2017-01-25 21:59:25 +01:00
$fields -> makeFieldReadonly ( $viewerGroupsField );
} else {
$fields -> removeByName ( 'ViewerGroups' );
}
$fields -> makeFieldReadonly ( $editorsOptionsField );
2017-05-12 02:47:46 +02:00
if ( $this -> CanEditType === InheritedPermissions :: ONLY_THESE_USERS ) {
2017-01-25 21:59:25 +01:00
$fields -> makeFieldReadonly ( $editorGroupsField );
} else {
$fields -> removeByName ( 'EditorGroups' );
}
}
if ( self :: $runCMSFieldsExtensions ) {
$this -> extend ( 'updateSettingsFields' , $fields );
}
return $fields ;
}
/**
* @ param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
* @ return array
*/
public function fieldLabels ( $includerelations = true )
{
$cacheKey = static :: class . '_' . $includerelations ;
if ( ! isset ( self :: $_cache_field_labels [ $cacheKey ])) {
$labels = parent :: fieldLabels ( $includerelations );
2017-05-08 07:57:24 +02:00
$labels [ 'Title' ] = _t ( __CLASS__ . '.PAGETITLE' , " Page name " );
$labels [ 'MenuTitle' ] = _t ( __CLASS__ . '.MENUTITLE' , " Navigation label " );
$labels [ 'MetaDescription' ] = _t ( __CLASS__ . '.METADESC' , " Meta Description " );
$labels [ 'ExtraMeta' ] = _t ( __CLASS__ . '.METAEXTRA' , " Custom Meta Tags " );
$labels [ 'ClassName' ] = _t ( __CLASS__ . '.PAGETYPE' , " Page type " , 'Classname of a page object' );
$labels [ 'ParentType' ] = _t ( __CLASS__ . '.PARENTTYPE' , " Page location " );
$labels [ 'ParentID' ] = _t ( __CLASS__ . '.PARENTID' , " Parent page " );
$labels [ 'ShowInMenus' ] = _t ( __CLASS__ . '.SHOWINMENUS' , " Show in menus? " );
$labels [ 'ShowInSearch' ] = _t ( __CLASS__ . '.SHOWINSEARCH' , " Show in search? " );
$labels [ 'ViewerGroups' ] = _t ( __CLASS__ . '.VIEWERGROUPS' , " Viewer Groups " );
$labels [ 'EditorGroups' ] = _t ( __CLASS__ . '.EDITORGROUPS' , " Editor Groups " );
2019-08-14 18:38:36 +02:00
$labels [ 'URLSegment' ] = _t ( __CLASS__ . '.URLSegment' , 'URL segment' , 'URL for this page' );
2017-05-08 07:57:24 +02:00
$labels [ 'Content' ] = _t ( __CLASS__ . '.Content' , 'Content' , 'Main HTML Content for a page' );
$labels [ 'CanViewType' ] = _t ( __CLASS__ . '.Viewers' , 'Viewers Groups' );
$labels [ 'CanEditType' ] = _t ( __CLASS__ . '.Editors' , 'Editors Groups' );
$labels [ 'Comments' ] = _t ( __CLASS__ . '.Comments' , 'Comments' );
$labels [ 'Visibility' ] = _t ( __CLASS__ . '.Visibility' , 'Visibility' );
2017-01-25 21:59:25 +01:00
$labels [ 'LinkChangeNote' ] = _t (
2018-04-06 05:53:57 +02:00
__CLASS__ . '.LINKCHANGENOTE' ,
2017-01-25 21:59:25 +01:00
'Changing this page\'s link will also affect the links of all child pages.'
);
if ( $includerelations ) {
2017-05-08 07:57:24 +02:00
$labels [ 'Parent' ] = _t ( __CLASS__ . '.has_one_Parent' , 'Parent Page' , 'The parent page in the site hierarchy' );
$labels [ 'LinkTracking' ] = _t ( __CLASS__ . '.many_many_LinkTracking' , 'Link Tracking' );
2018-04-06 05:53:57 +02:00
$labels [ 'FileTracking' ] = _t ( __CLASS__ . '.many_many_ImageTracking' , 'Image Tracking' );
2017-05-08 07:57:24 +02:00
$labels [ 'BackLinkTracking' ] = _t ( __CLASS__ . '.many_many_BackLinkTracking' , 'Backlink Tracking' );
2017-01-25 21:59:25 +01:00
}
self :: $_cache_field_labels [ $cacheKey ] = $labels ;
}
return self :: $_cache_field_labels [ $cacheKey ];
}
/**
* Get the actions available in the CMS for this page - eg Save , Publish .
*
* Frontend scripts and styles know how to handle the following FormFields :
* - top - level FormActions appear as standalone buttons
* - top - level CompositeField with FormActions within appear as grouped buttons
* - TabSet & Tabs appear as a drop ups
* - FormActions within the Tab are restyled as links
* - major actions can provide alternate states for richer presentation ( see ssui . button widget extension )
*
* @ return FieldList The available actions for this page .
*/
public function getCMSActions ()
{
// Get status of page
$isOnDraft = $this -> isOnDraft ();
$isPublished = $this -> isPublished ();
2018-04-06 05:53:57 +02:00
$stagesDiffer = $this -> stagesDiffer ();
2017-01-25 21:59:25 +01:00
// Check permissions
$canPublish = $this -> canPublish ();
$canUnpublish = $this -> canUnpublish ();
$canEdit = $this -> canEdit ();
// Major actions appear as buttons immediately visible as page actions.
$majorActions = CompositeField :: create () -> setName ( 'MajorActions' );
$majorActions -> setFieldHolderTemplate ( get_class ( $majorActions ) . '_holder_buttongroup' );
// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
$rootTabSet = new TabSet ( 'ActionMenus' );
$moreOptions = new Tab (
'MoreOptions' ,
2017-05-08 07:57:24 +02:00
_t ( __CLASS__ . '.MoreOptions' , 'More options' , 'Expands a view for more buttons' )
2017-01-25 21:59:25 +01:00
);
2017-06-19 02:17:05 +02:00
$moreOptions -> addExtraClass ( 'popover-actions-simulate' );
2017-01-25 21:59:25 +01:00
$rootTabSet -> push ( $moreOptions );
$rootTabSet -> addExtraClass ( 'ss-ui-action-tabset action-menus noborder' );
// Render page information into the "more-options" drop-up, on the top.
$liveRecord = Versioned :: get_by_stage ( self :: class , Versioned :: LIVE ) -> byID ( $this -> ID );
$infoTemplate = SSViewer :: get_templates_by_class ( static :: class , '_Information' , self :: class );
$moreOptions -> push (
new LiteralField (
'Information' ,
2020-04-19 06:18:01 +02:00
$this -> customise ([
2017-01-25 21:59:25 +01:00
'Live' => $liveRecord ,
'ExistsOnLive' => $isPublished
2020-04-19 06:18:01 +02:00
]) -> renderWith ( $infoTemplate )
2017-01-25 21:59:25 +01:00
)
);
// "readonly"/viewing version that isn't the current version of the record
2018-04-06 05:53:57 +02:00
/** @var SiteTree $stageRecord */
2017-01-25 21:59:25 +01:00
$stageRecord = Versioned :: get_by_stage ( static :: class , Versioned :: DRAFT ) -> byID ( $this -> ID );
/** @skipUpgrade */
if ( $stageRecord && $stageRecord -> Version != $this -> Version ) {
2017-05-08 07:57:24 +02:00
$moreOptions -> push ( FormAction :: create ( 'email' , _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.EMAIL' , 'Email' )));
$moreOptions -> push ( FormAction :: create ( 'rollback' , _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.ROLLBACK' , 'Roll back to this version' )));
2020-04-19 06:18:01 +02:00
$actions = new FieldList ([ $majorActions , $rootTabSet ]);
2017-01-25 21:59:25 +01:00
// getCMSActions() can be extended with updateCMSActions() on a extension
$this -> extend ( 'updateCMSActions' , $actions );
return $actions ;
}
// "unpublish"
2020-08-31 02:48:43 +02:00
if ( $isPublished && $isOnDraft && $canUnpublish ) {
2017-01-25 21:59:25 +01:00
$moreOptions -> push (
2022-01-21 01:27:02 +01:00
FormAction :: create ( 'unpublish' , _t ( __CLASS__ . '.BUTTONUNPUBLISH' , 'Unpublish' ))
2017-05-08 07:57:24 +02:00
-> setDescription ( _t ( __CLASS__ . '.BUTTONUNPUBLISHDESC' , 'Remove this page from the published site' ))
2019-08-16 05:25:21 +02:00
-> addExtraClass ( 'btn-secondary' . ( $this -> isHomePage () ? ' homepage-warning' : '' ))
2017-01-25 21:59:25 +01:00
);
}
// "rollback"
if ( $isOnDraft && $isPublished && $canEdit && $stagesDiffer ) {
$moreOptions -> push (
2017-05-08 07:57:24 +02:00
FormAction :: create ( 'rollback' , _t ( __CLASS__ . '.BUTTONCANCELDRAFT' , 'Cancel draft changes' ))
2017-01-25 21:59:25 +01:00
-> setDescription ( _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.BUTTONCANCELDRAFTDESC' ,
2017-01-25 21:59:25 +01:00
'Delete your draft and revert to the currently published page'
))
-> addExtraClass ( 'btn-secondary' )
);
}
// "restore"
if ( $canEdit && ! $isOnDraft && $isPublished ) {
2018-07-13 00:59:28 +02:00
$majorActions -> push (
FormAction :: create ( 'revert' , _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE' , 'Restore' ))
-> addExtraClass ( 'btn-warning font-icon-back-in-time' )
-> setUseButtonTag ( true )
);
2017-01-25 21:59:25 +01:00
}
// Check if we can restore a deleted page
// Note: It would be nice to have a canRestore() permission at some point
if ( $canEdit && ! $isOnDraft && ! $isPublished ) {
// Determine if we should force a restore to root (where once it was a subpage)
$restoreToRoot = $this -> isParentArchived ();
// "restore"
$title = $restoreToRoot
2017-04-20 03:15:29 +02:00
? _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT' , 'Restore draft at top level' )
: _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE' , 'Restore draft' );
2017-01-25 21:59:25 +01:00
$description = $restoreToRoot
2017-04-20 03:15:29 +02:00
? _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT_DESC' , 'Restore the archived version to draft as a top level page' )
: _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_DESC' , 'Restore the archived version to draft' );
2017-01-25 21:59:25 +01:00
$majorActions -> push (
FormAction :: create ( 'restore' , $title )
-> setDescription ( $description )
-> setAttribute ( 'data-to-root' , $restoreToRoot )
2018-02-07 02:06:21 +01:00
-> addExtraClass ( 'btn-warning font-icon-back-in-time' )
-> setUseButtonTag ( true )
2017-01-25 21:59:25 +01:00
);
}
// If a page is on any stage it can be archived
if (( $isOnDraft || $isPublished ) && $this -> canArchive ()) {
$title = $isPublished
2017-04-20 03:15:29 +02:00
? _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.UNPUBLISH_AND_ARCHIVE' , 'Unpublish and archive' )
: _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVE' , 'Archive' );
2017-01-25 21:59:25 +01:00
$moreOptions -> push (
FormAction :: create ( 'archive' , $title )
2019-08-16 05:25:21 +02:00
-> addExtraClass ( 'delete btn btn-secondary' . ( $this -> isHomePage () ? ' homepage-warning' : '' ))
2017-01-25 21:59:25 +01:00
-> setDescription ( _t (
2017-04-20 03:15:29 +02:00
'SilverStripe\\CMS\\Model\\SiteTree.BUTTONDELETEDESC' ,
2017-01-25 21:59:25 +01:00
'Remove from draft/live and send to archive'
))
);
}
// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2017-09-11 06:26:16 +02:00
$noChangesClasses = 'btn-outline-primary font-icon-tick' ;
2017-01-25 21:59:25 +01:00
if ( $canEdit && $isOnDraft ) {
$majorActions -> push (
2017-05-08 07:57:24 +02:00
FormAction :: create ( 'save' , _t ( __CLASS__ . '.BUTTONSAVED' , 'Saved' ))
2017-09-05 06:36:20 +02:00
-> addExtraClass ( $noChangesClasses )
-> setAttribute ( 'data-btn-alternate-add' , 'btn-primary font-icon-save' )
-> setAttribute ( 'data-btn-alternate-remove' , $noChangesClasses )
2017-01-25 21:59:25 +01:00
-> setUseButtonTag ( true )
2018-02-09 00:18:57 +01:00
-> setAttribute ( 'data-text-alternate' , _t ( 'SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT' , 'Save' ))
2017-01-25 21:59:25 +01:00
);
}
if ( $canPublish && $isOnDraft ) {
// "publish", as with "save", it supports an alternate state to show when action is needed.
$majorActions -> push (
2017-05-08 07:57:24 +02:00
$publish = FormAction :: create ( 'publish' , _t ( __CLASS__ . '.BUTTONPUBLISHED' , 'Published' ))
2017-09-05 06:36:20 +02:00
-> addExtraClass ( $noChangesClasses )
-> setAttribute ( 'data-btn-alternate-add' , 'btn-primary font-icon-rocket' )
-> setAttribute ( 'data-btn-alternate-remove' , $noChangesClasses )
2017-01-25 21:59:25 +01:00
-> setUseButtonTag ( true )
2018-02-09 00:18:57 +01:00
-> setAttribute ( 'data-text-alternate' , _t ( __CLASS__ . '.BUTTONSAVEPUBLISH' , 'Publish' ))
2017-01-25 21:59:25 +01:00
);
// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
if ( $stagesDiffer ) {
$publish -> addExtraClass ( 'btn-primary font-icon-rocket' );
2018-02-09 00:18:57 +01:00
$publish -> setTitle ( _t ( __CLASS__ . '.BUTTONSAVEPUBLISH' , 'Publish' ));
2017-09-05 06:36:20 +02:00
$publish -> removeExtraClass ( $noChangesClasses );
2017-01-25 21:59:25 +01:00
}
}
2020-04-19 06:18:01 +02:00
$actions = new FieldList ([ $majorActions , $rootTabSet ]);
2017-01-25 21:59:25 +01:00
// Hook for extensions to add/remove actions.
$this -> extend ( 'updateCMSActions' , $actions );
return $actions ;
}
public function onAfterPublish ()
{
// Force live sort order to match stage sort order
2019-05-17 03:40:15 +02:00
$sql = sprintf (
' UPDATE " %2 $s "
SET " Sort " = ( SELECT " %1 $s " . " Sort " FROM " %1 $s " WHERE " %2 $s " . " ID " = " %1 $s " . " ID " )
WHERE EXISTS ( SELECT " %1 $s " . " Sort " FROM " %1 $s " WHERE " %2 $s " . " ID " = " %1 $s " . " ID " ) AND " ParentID " = ? ' ,
$this -> baseTable (),
$this -> stageTable ( $this -> baseTable (), Versioned :: LIVE )
2017-01-25 21:59:25 +01:00
);
2019-05-17 03:40:15 +02:00
DB :: prepared_query ( $sql , [ $this -> ParentID ]);
2017-01-25 21:59:25 +01:00
}
/**
* Update draft dependant pages
*/
public function onAfterRevertToLive ()
{
// Use an alias to get the updates made by $this->publish
/** @var SiteTree $stageSelf */
$stageSelf = Versioned :: get_by_stage ( self :: class , Versioned :: DRAFT ) -> byID ( $this -> ID );
$stageSelf -> writeWithoutVersion ();
// Need to update pages linking to this one as no longer broken
foreach ( $stageSelf -> DependentPages () as $page ) {
/** @var SiteTree $page */
$page -> writeWithoutVersion ();
}
}
/**
* Determine if this page references a parent which is archived , and not available in stage
*
* @ return bool True if there is an archived parent
*/
protected function isParentArchived ()
{
if ( $parentID = $this -> ParentID ) {
/** @var SiteTree $parentPage */
$parentPage = Versioned :: get_latest_version ( self :: class , $parentID );
if ( ! $parentPage || ! $parentPage -> isOnDraft ()) {
return true ;
}
}
return false ;
}
/**
* Restore the content in the active copy of this SiteTree page to the stage site .
*
2018-03-14 04:34:46 +01:00
* @ return static
2017-01-25 21:59:25 +01:00
*/
public function doRestoreToStage ()
{
$this -> invokeWithExtensions ( 'onBeforeRestoreToStage' , $this );
// Ensure that the parent page is restored, otherwise restore to root
if ( $this -> isParentArchived ()) {
$this -> ParentID = 0 ;
}
2018-03-14 04:34:46 +01:00
// Restore
$this -> writeToStage ( Versioned :: DRAFT );
2017-01-25 21:59:25 +01:00
2017-06-21 06:29:40 +02:00
// Need to update pages linking to this one as no longer broken
2018-03-14 04:34:46 +01:00
/** @var SiteTree $result */
$result = Versioned :: get_by_stage ( self :: class , Versioned :: DRAFT )
-> byID ( $this -> ID );
$result -> updateDependentPages ();
2017-06-21 06:29:40 +02:00
2018-03-14 04:34:46 +01:00
$this -> invokeWithExtensions ( 'onAfterRestoreToStage' , $result );
2017-01-25 21:59:25 +01:00
return $result ;
}
/**
* Check if this page is new - that is , if it has yet to have been written to the database .
*
* @ return bool
*/
public function isNew ()
{
/**
* This check was a problem for a self - hosted site , and may indicate a bug in the interpreter on their server ,
* or a bug here . Changing the condition from empty ( $this -> ID ) to ! $this -> ID && ! $this -> record [ 'ID' ] fixed this .
*/
if ( empty ( $this -> ID )) {
return true ;
}
if ( is_numeric ( $this -> ID )) {
return false ;
}
2022-04-13 07:07:59 +02:00
return stripos ( $this -> ID ? ? '' , 'new' ) === 0 ;
2017-01-25 21:59:25 +01:00
}
/**
* Get the class dropdown used in the CMS to change the class of a page . This returns the list of options in the
* dropdown as a Map from class name to singular name . Filters by { @ link SiteTree -> canCreate ()}, as well as
* { @ link SiteTree :: $needs_permission } .
*
* @ return array
*/
protected function getClassDropdown ()
{
$classes = self :: page_type_classes ();
$currentClass = null ;
2020-04-19 06:18:01 +02:00
$result = [];
2017-01-25 21:59:25 +01:00
foreach ( $classes as $class ) {
$instance = singleton ( $class );
2019-05-17 02:53:59 +02:00
//if the current page is root and the instance can't be, exclude
if ( ! $instance -> config () -> get ( 'can_be_root' ) && $this -> ParentID == 0 ) {
continue ;
}
2017-01-25 21:59:25 +01:00
// if the current page type is this the same as the class type always show the page type in the list
if ( $this -> ClassName != $instance -> ClassName ) {
if ( $instance instanceof HiddenClass ) {
continue ;
}
2020-04-19 06:18:01 +02:00
if ( ! $instance -> canCreate ( null , [ 'Parent' => $this -> ParentID ? $this -> Parent () : null ])) {
2017-01-25 21:59:25 +01:00
continue ;
}
}
2017-08-22 23:46:46 +02:00
if ( $perms = $instance -> config () -> get ( 'need_permission' )) {
2017-01-25 21:59:25 +01:00
if ( ! $this -> can ( $perms )) {
continue ;
}
}
$pageTypeName = $instance -> i18n_singular_name ();
$currentClass = $class ;
$result [ $class ] = $pageTypeName ;
}
// sort alphabetically, and put current on top
asort ( $result );
if ( $currentClass ) {
$currentPageTypeName = $result [ $currentClass ];
unset ( $result [ $currentClass ]);
2022-04-13 07:07:59 +02:00
$result = array_reverse ( $result ? ? []);
2017-01-25 21:59:25 +01:00
$result [ $currentClass ] = $currentPageTypeName ;
2022-04-13 07:07:59 +02:00
$result = array_reverse ( $result ? ? []);
2017-01-25 21:59:25 +01:00
}
return $result ;
}
/**
* Returns an array of the class names of classes that are allowed to be children of this class .
*
* @ return string []
*/
public function allowedChildren ()
{
2017-02-10 11:31:25 +01:00
if ( isset ( static :: $_allowedChildren [ $this -> ClassName ])) {
2017-02-14 14:48:48 +01:00
$allowedChildren = static :: $_allowedChildren [ $this -> ClassName ];
} else {
// Get config based on old FIRST_SET rules
$candidates = null ;
$class = get_class ( $this );
while ( $class ) {
if ( Config :: inst () -> exists ( $class , 'allowed_children' , Config :: UNINHERITED )) {
$candidates = Config :: inst () -> get ( $class , 'allowed_children' , Config :: UNINHERITED );
break ;
}
2022-04-13 07:07:59 +02:00
$class = get_parent_class ( $class ? ? '' );
2017-02-14 14:48:48 +01:00
}
if ( ! $candidates || $candidates === 'none' || $candidates === 'SiteTree_root' ) {
return [];
2017-02-28 03:46:07 +01:00
}
2017-02-14 14:48:48 +01:00
// Parse candidate list
$allowedChildren = [];
2017-09-20 03:51:07 +02:00
foreach (( array ) $candidates as $candidate ) {
2017-02-14 14:48:48 +01:00
// If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
// Otherwise, the class and all its subclasses are allowed.
2022-04-13 07:07:59 +02:00
if ( substr ( $candidate ? ? '' , 0 , 1 ) == '*' ) {
$allowedChildren [] = substr ( $candidate ? ? '' , 1 );
2017-09-20 03:51:07 +02:00
} elseif (( $candidate !== 'SiteTree_root' )
&& ( $subclasses = ClassInfo :: subclassesFor ( $candidate ))
) {
2017-02-14 14:48:48 +01:00
foreach ( $subclasses as $subclass ) {
2017-09-20 03:51:07 +02:00
if ( ! is_a ( $subclass , HiddenClass :: class , true )) {
$allowedChildren [] = $subclass ;
2017-02-14 14:48:48 +01:00
}
2017-01-25 21:59:25 +01:00
}
}
2017-02-14 14:48:48 +01:00
static :: $_allowedChildren [ get_class ( $this )] = $allowedChildren ;
2017-01-25 21:59:25 +01:00
}
}
2017-02-14 14:48:48 +01:00
$this -> extend ( 'updateAllowedChildren' , $allowedChildren );
2017-02-10 11:31:25 +01:00
2017-02-14 14:48:48 +01:00
return $allowedChildren ;
2017-01-25 21:59:25 +01:00
}
2017-11-30 03:56:16 +01:00
/**
2022-10-18 04:23:59 +02:00
* @ deprecated 4.12 . 0 Use creatableChildPages () instead
2019-02-25 03:11:05 +01:00
*
2017-11-30 03:56:16 +01:00
* Gets a list of the page types that can be created under this specific page
*
* @ return array
*/
public function creatableChildren ()
{
2022-10-18 04:23:59 +02:00
Deprecation :: notice ( '4.12.0' , 'Use creatableChildPages() instead' );
2017-11-30 03:56:16 +01:00
// Build the list of candidate children
2017-12-13 03:36:35 +01:00
$cache = SiteTree :: singleton () -> getCreatableChildrenCache ();
2017-11-30 03:56:16 +01:00
$cacheKey = $this -> generateChildrenCacheKey ( Security :: getCurrentUser () ? Security :: getCurrentUser () -> ID : 0 );
$children = $cache -> get ( $cacheKey , []);
if ( ! $children || ! isset ( $children [ $this -> ID ])) {
$children [ $this -> ID ] = [];
$candidates = static :: page_type_classes ();
foreach ( $candidates as $childClass ) {
$child = singleton ( $childClass );
if ( $child -> canCreate ( null , [ 'Parent' => $this ])) {
$children [ $this -> ID ][ $childClass ] = $child -> i18n_singular_name ();
}
}
$cache -> set ( $cacheKey , $children );
}
return $children [ $this -> ID ];
}
2019-02-25 03:11:05 +01:00
/**
*
* Gets a list of the page types that can be created under this specific page , including font icons
*
* @ return array
*/
public function creatableChildPages ()
{
// Build the list of candidate children
$cache = SiteTree :: singleton () -> getCreatableChildrenCache ();
$cacheKey = $this -> generateChildrenCacheKey ( Security :: getCurrentUser () ? Security :: getCurrentUser () -> ID : 0 );
$children = $cache -> get ( $cacheKey , []);
if ( ! $children || ! isset ( $children [ $this -> ID ])) {
$children [ $this -> ID ] = [];
$candidates = static :: page_type_classes ();
foreach ( $candidates as $childClass ) {
$child = singleton ( $childClass );
if ( $child -> canCreate ( null , [ 'Parent' => $this ])) {
$children [ $this -> ID ][] = [
'ClassName' => $childClass ,
'Title' => $child -> i18n_singular_name (),
'IconClass' => $child -> getIconClass (),
];
}
}
$cache -> set ( $cacheKey , $children );
}
return $children [ $this -> ID ];
}
2017-01-25 21:59:25 +01:00
/**
* Returns the class name of the default class for children of this page .
*
* @ return string
*/
public function defaultChild ()
{
2017-08-22 23:46:46 +02:00
$default = $this -> config () -> get ( 'default_child' );
2017-01-25 21:59:25 +01:00
$allowed = $this -> allowedChildren ();
if ( $allowed ) {
2022-04-13 07:07:59 +02:00
if ( ! $default || ! in_array ( $default , $allowed ? ? [])) {
2017-01-25 21:59:25 +01:00
$default = reset ( $allowed );
}
return $default ;
}
return null ;
}
/**
* Returns the class name of the default class for the parent of this page .
*
* @ return string
*/
public function defaultParent ()
{
2017-08-22 23:46:46 +02:00
return $this -> config () -> get ( 'default_parent' );
2017-01-25 21:59:25 +01:00
}
/**
* Get the title for use in menus for this page . If the MenuTitle field is set it returns that , else it returns the
* Title field .
*
* @ return string
*/
public function getMenuTitle ()
{
if ( $value = $this -> getField ( " MenuTitle " )) {
return $value ;
} else {
return $this -> getField ( " Title " );
}
}
/**
* Set the menu title for this page .
*
* @ param string $value
*/
public function setMenuTitle ( $value )
{
if ( $value == $this -> getField ( " Title " )) {
$this -> setField ( " MenuTitle " , null );
} else {
$this -> setField ( " MenuTitle " , $value );
}
}
/**
* A flag provides the user with additional data about the current page status , for example a " removed from draft "
* status . Each page can have more than one status flag . Returns a map of a unique key to a ( localized ) title for
* the flag . The unique key can be reused as a CSS class . Use the 'updateStatusFlags' extension point to customize
* the flags .
*
* Example ( simple ) :
* " deletedonlive " => " Deleted "
*
* Example ( with optional title attribute ) :
2020-04-19 06:18:01 +02:00
* " deletedonlive " => [ 'text' => " Deleted " , 'title' => 'This page has been deleted' ]
2017-01-25 21:59:25 +01:00
*
* @ param bool $cached Whether to serve the fields from cache ; false regenerate them
* @ return array
*/
public function getStatusFlags ( $cached = true )
{
if ( ! $this -> _cache_statusFlags || ! $cached ) {
2020-04-19 06:18:01 +02:00
$flags = [];
2017-01-25 21:59:25 +01:00
if ( $this -> isOnLiveOnly ()) {
2020-04-19 06:18:01 +02:00
$flags [ 'removedfromdraft' ] = [
2017-05-08 07:57:24 +02:00
'text' => _t ( __CLASS__ . '.ONLIVEONLYSHORT' , 'On live only' ),
'title' => _t ( __CLASS__ . '.ONLIVEONLYSHORTHELP' , 'Page is published, but has been deleted from draft' ),
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
} elseif ( $this -> isArchived ()) {
2020-04-19 06:18:01 +02:00
$flags [ 'archived' ] = [
2017-05-08 07:57:24 +02:00
'text' => _t ( __CLASS__ . '.ARCHIVEDPAGESHORT' , 'Archived' ),
'title' => _t ( __CLASS__ . '.ARCHIVEDPAGEHELP' , 'Page is removed from draft and live' ),
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
} elseif ( $this -> isOnDraftOnly ()) {
2020-04-19 06:18:01 +02:00
$flags [ 'addedtodraft' ] = [
2017-05-08 07:57:24 +02:00
'text' => _t ( __CLASS__ . '.ADDEDTODRAFTSHORT' , 'Draft' ),
'title' => _t ( __CLASS__ . '.ADDEDTODRAFTHELP' , " Page has not been published yet " )
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
} elseif ( $this -> isModifiedOnDraft ()) {
2020-04-19 06:18:01 +02:00
$flags [ 'modified' ] = [
2017-05-08 07:57:24 +02:00
'text' => _t ( __CLASS__ . '.MODIFIEDONDRAFTSHORT' , 'Modified' ),
'title' => _t ( __CLASS__ . '.MODIFIEDONDRAFTHELP' , 'Page has unpublished changes' ),
2020-04-19 06:18:01 +02:00
];
2017-01-25 21:59:25 +01:00
}
$this -> extend ( 'updateStatusFlags' , $flags );
$this -> _cache_statusFlags = $flags ;
}
return $this -> _cache_statusFlags ;
}
2019-02-25 03:11:05 +01:00
/**
* Returns the CSS class used for the page icon in the site tree .
*
* @ return string
*/
public function getIconClass ()
{
if ( $this -> config () -> get ( 'icon' )) {
return '' ;
2019-04-08 01:45:44 +02:00
}
2019-04-08 01:10:07 +02:00
return $this -> config () -> get ( 'icon_class' );
2019-02-25 03:11:05 +01:00
}
2017-01-25 21:59:25 +01:00
/**
* getTreeTitle will return three < span > html DOM elements , an empty < span > with the class ' jstree - pageicon ' in
2019-02-25 03:11:05 +01:00
* front , following by a < span > wrapping around its MenuTitle , then following by a < span > indicating its
2017-01-25 21:59:25 +01:00
* publication status .
*
* @ return string An HTML string ready to be directly used in a template
*/
public function getTreeTitle ()
{
2019-02-25 03:11:05 +01:00
$children = $this -> creatableChildPages ();
2017-01-25 21:59:25 +01:00
$flags = $this -> getStatusFlags ();
$treeTitle = sprintf (
2019-08-16 05:25:21 +02:00
'<span class="jstree-pageicon page-icon %s class-%s%s"></span><span class="item" data-allowedchildren="%s">%s</span>' ,
2019-02-25 03:11:05 +01:00
$this -> getIconClass (),
2017-08-24 11:39:25 +02:00
Convert :: raw2htmlid ( static :: class ),
2019-08-16 05:25:21 +02:00
$this -> isHomePage () ? ' homepage' : '' ,
2018-10-28 22:21:19 +01:00
Convert :: raw2att ( json_encode ( $children )),
2022-04-13 07:07:59 +02:00
Convert :: raw2xml ( str_replace ([ " \n " , " \r " ], " " , $this -> MenuTitle ? ? '' ))
2017-01-25 21:59:25 +01:00
);
foreach ( $flags as $class => $data ) {
if ( is_string ( $data )) {
2020-04-19 06:18:01 +02:00
$data = [ 'text' => $data ];
2017-01-25 21:59:25 +01:00
}
$treeTitle .= sprintf (
" <span class= \" badge %s \" %s>%s</span> " ,
'status-' . Convert :: raw2xml ( $class ),
( isset ( $data [ 'title' ])) ? sprintf ( ' title="%s"' , Convert :: raw2xml ( $data [ 'title' ])) : '' ,
Convert :: raw2xml ( $data [ 'text' ])
);
}
return $treeTitle ;
}
/**
* Returns the page in the current page stack of the given level . Level ( 1 ) will return the main menu item that
* we ' re currently inside , etc .
*
* @ param int $level
* @ return SiteTree
*/
public function Level ( $level )
{
$parent = $this ;
2020-04-19 06:18:01 +02:00
$stack = [ $parent ];
2017-01-25 21:59:25 +01:00
while (( $parent = $parent -> Parent ()) && $parent -> exists ()) {
array_unshift ( $stack , $parent );
}
return isset ( $stack [ $level - 1 ]) ? $stack [ $level - 1 ] : null ;
}
/**
* Gets the depth of this page in the sitetree , where 1 is the root level
*
* @ return int
*/
public function getPageLevel ()
{
if ( $this -> ParentID ) {
return 1 + $this -> Parent () -> getPageLevel ();
}
return 1 ;
}
/**
* Find the controller name by our convention of { $ModelClass } Controller
2018-03-01 16:31:50 +01:00
* Can be overriden by config variable
2017-01-25 21:59:25 +01:00
*
* @ return string
*/
public function getControllerName ()
{
2018-03-01 16:31:50 +01:00
if ( $controller = Config :: inst () -> get ( static :: class , 'controller_name' )) {
return $controller ;
}
2019-10-15 13:29:05 +02:00
$namespaceMap = Config :: inst () -> get ( SiteTree :: class , 'namespace_mapping' );
2017-01-25 21:59:25 +01:00
//default controller for SiteTree objects
$controller = ContentController :: class ;
//go through the ancestry for this class looking for
$ancestry = ClassInfo :: ancestry ( static :: class );
// loop over the array going from the deepest descendant (ie: the current class) to SiteTree
while ( $class = array_pop ( $ancestry )) {
//we don't need to go any deeper than the SiteTree class
if ( $class == SiteTree :: class ) {
break ;
}
// If we have a class of "{$ClassName}Controller" then we found our controller
if ( class_exists ( $candidate = sprintf ( '%sController' , $class ))) {
2019-10-15 13:29:05 +02:00
return $candidate ;
2017-01-25 21:59:25 +01:00
} elseif ( class_exists ( $candidate = sprintf ( '%s_Controller' , $class ))) {
// Support the legacy underscored filename, but raise a deprecation notice
Deprecation :: notice (
'5.0' ,
'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".' ,
Deprecation :: SCOPE_GLOBAL
);
2019-10-15 13:29:05 +02:00
return $candidate ;
} elseif ( is_array ( $namespaceMap )) {
foreach ( $namespaceMap as $pageNamespace => $controllerNamespace ) {
if ( strpos ( $class , $pageNamespace ) !== 0 ) {
continue ;
}
$candidate = sprintf (
'%sController' ,
str_replace ( $pageNamespace , $controllerNamespace , $class )
);
if ( class_exists ( $candidate )) {
return $candidate ;
}
}
2017-01-25 21:59:25 +01:00
}
}
return $controller ;
}
/**
* Return the CSS classes to apply to this node in the CMS tree .
*
* @ return string
*/
2017-04-06 07:38:15 +02:00
public function CMSTreeClasses ()
2017-01-25 21:59:25 +01:00
{
2017-08-24 11:39:25 +02:00
$classes = sprintf ( 'class-%s' , Convert :: raw2htmlid ( static :: class ));
2017-01-25 21:59:25 +01:00
if ( $this -> HasBrokenFile || $this -> HasBrokenLink ) {
$classes .= " BrokenLink " ;
}
if ( ! $this -> canAddChildren ()) {
$classes .= " nochildren " ;
}
if ( ! $this -> canEdit () && ! $this -> canAddChildren ()) {
if ( ! $this -> canView ()) {
$classes .= " disabled " ;
} else {
$classes .= " edit-disabled " ;
}
}
if ( ! $this -> ShowInMenus ) {
$classes .= " notinmenu " ;
}
return $classes ;
}
/**
* Stops extendCMSFields () being called on getCMSFields () . This is useful when you need access to fields added by
* subclasses of SiteTree in a extension . Call before calling parent :: getCMSFields (), and reenable afterwards .
*/
public static function disableCMSFieldsExtensions ()
{
self :: $runCMSFieldsExtensions = false ;
}
/**
* Reenables extendCMSFields () being called on getCMSFields () after it has been disabled by
* disableCMSFieldsExtensions () .
*/
public static function enableCMSFieldsExtensions ()
{
self :: $runCMSFieldsExtensions = true ;
}
public function providePermissions ()
{
2020-04-19 06:18:01 +02:00
return [
'SITETREE_GRANT_ACCESS' => [
2017-05-08 07:57:24 +02:00
'name' => _t ( __CLASS__ . '.PERMISSION_GRANTACCESS_DESCRIPTION' , 'Manage access rights for content' ),
'help' => _t ( __CLASS__ . '.PERMISSION_GRANTACCESS_HELP' , 'Allow setting of page-specific access restrictions in the "Pages" section.' ),
2017-04-20 03:45:23 +02:00
'category' => _t ( 'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY' , 'Roles and access permissions' ),
2017-01-25 21:59:25 +01:00
'sort' => 100
2020-04-19 06:18:01 +02:00
],
'SITETREE_VIEW_ALL' => [
2017-05-08 07:57:24 +02:00
'name' => _t ( __CLASS__ . '.VIEW_ALL_DESCRIPTION' , 'View any page' ),
2017-04-20 03:45:23 +02:00
'category' => _t ( 'SilverStripe\\Security\\Permission.CONTENT_CATEGORY' , 'Content permissions' ),
2017-01-25 21:59:25 +01:00
'sort' => - 100 ,
2017-05-08 07:57:24 +02:00
'help' => _t ( __CLASS__ . '.VIEW_ALL_HELP' , 'Ability to view any page on the site, regardless of the settings on the Access tab. Requires the "Access to \'Pages\' section" permission' )
2020-04-19 06:18:01 +02:00
],
'SITETREE_EDIT_ALL' => [
2017-05-08 07:57:24 +02:00
'name' => _t ( __CLASS__ . '.EDIT_ALL_DESCRIPTION' , 'Edit any page' ),
2017-04-20 03:45:23 +02:00
'category' => _t ( 'SilverStripe\\Security\\Permission.CONTENT_CATEGORY' , 'Content permissions' ),
2017-01-25 21:59:25 +01:00
'sort' => - 50 ,
2017-05-08 07:57:24 +02:00
'help' => _t ( __CLASS__ . '.EDIT_ALL_HELP' , 'Ability to edit any page on the site, regardless of the settings on the Access tab. Requires the "Access to \'Pages\' section" permission' )
2020-04-19 06:18:01 +02:00
],
'SITETREE_REORGANISE' => [
2017-05-08 07:57:24 +02:00
'name' => _t ( __CLASS__ . '.REORGANISE_DESCRIPTION' , 'Change site structure' ),
2017-04-20 03:45:23 +02:00
'category' => _t ( 'SilverStripe\\Security\\Permission.CONTENT_CATEGORY' , 'Content permissions' ),
2017-05-08 07:57:24 +02:00
'help' => _t ( __CLASS__ . '.REORGANISE_HELP' , 'Rearrange pages in the site tree through drag&drop.' ),
2017-01-25 21:59:25 +01:00
'sort' => 100
2020-04-19 06:18:01 +02:00
],
'VIEW_DRAFT_CONTENT' => [
2017-05-08 07:57:24 +02:00
'name' => _t ( __CLASS__ . '.VIEW_DRAFT_CONTENT' , 'View draft content' ),
2017-04-20 03:45:23 +02:00
'category' => _t ( 'SilverStripe\\Security\\Permission.CONTENT_CATEGORY' , 'Content permissions' ),
2017-05-08 07:57:24 +02:00
'help' => _t ( __CLASS__ . '.VIEW_DRAFT_CONTENT_HELP' , 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.' ),
2017-01-25 21:59:25 +01:00
'sort' => 100
2020-04-19 06:18:01 +02:00
]
];
2017-01-25 21:59:25 +01:00
}
/**
* Default singular name for page / sitetree
*
* @ return string
*/
public function singular_name ()
{
$base = in_array ( static :: class , [ Page :: class , self :: class ]);
if ( $base ) {
2017-08-22 23:46:46 +02:00
return $this -> config () -> get ( 'base_singular_name' );
2017-01-25 21:59:25 +01:00
}
return parent :: singular_name ();
}
/**
* Default plural name for page / sitetree
*
* @ return string
*/
public function plural_name ()
{
$base = in_array ( static :: class , [ Page :: class , self :: class ]);
if ( $base ) {
2017-08-22 23:46:46 +02:00
return $this -> config () -> get ( 'base_plural_name' );
2017-01-25 21:59:25 +01:00
}
return parent :: plural_name ();
}
2017-10-18 01:32:08 +02:00
/**
* Generate link to this page ' s icon
*
* @ return string
*/
public function getPageIconURL ()
{
$icon = $this -> config () -> get ( 'icon' );
if ( ! $icon ) {
return null ;
}
2022-04-13 07:07:59 +02:00
if ( strpos ( $icon ? ? '' , 'data:image/' ) !== false ) {
2017-11-22 20:55:35 +01:00
return $icon ;
}
2017-10-18 01:32:08 +02:00
// Icon is relative resource
$iconResource = ModuleResourceLoader :: singleton () -> resolveResource ( $icon );
if ( $iconResource instanceof ModuleResource ) {
return $iconResource -> getURL ();
}
// Full path to file
if ( Director :: fileExists ( $icon )) {
return ModuleResourceLoader :: resourceURL ( $icon );
}
// Skip invalid files
return null ;
}
2017-01-25 21:59:25 +01:00
/**
2017-03-29 00:55:44 +02:00
* Get description for this page type
2017-01-25 21:59:25 +01:00
*
* @ return string | null
*/
2017-03-29 00:55:44 +02:00
public function classDescription ()
2017-01-25 21:59:25 +01:00
{
$base = in_array ( static :: class , [ Page :: class , self :: class ]);
if ( $base ) {
2017-08-22 23:46:46 +02:00
return $this -> config () -> get ( 'base_description' );
2017-01-25 21:59:25 +01:00
}
2017-08-22 23:46:46 +02:00
return $this -> config () -> get ( 'description' );
2017-01-25 21:59:25 +01:00
}
/**
* Get localised description for this page
*
* @ return string | null
*/
2017-03-29 00:55:44 +02:00
public function i18n_classDescription ()
2017-01-25 21:59:25 +01:00
{
2017-03-29 00:55:44 +02:00
$description = $this -> classDescription ();
2017-01-25 21:59:25 +01:00
if ( $description ) {
return _t ( static :: class . '.DESCRIPTION' , $description );
}
return null ;
}
/**
* Overloaded to also provide entities for 'Page' class which is usually located in custom code , hence textcollector
* picks it up for the wrong folder .
*
* @ return array
*/
public function provideI18nEntities ()
{
$entities = parent :: provideI18nEntities ();
// Add optional description
2017-03-29 00:55:44 +02:00
$description = $this -> classDescription ();
2017-01-25 21:59:25 +01:00
if ( $description ) {
$entities [ static :: class . '.DESCRIPTION' ] = $description ;
}
return $entities ;
}
/**
* Returns 'root' if the current page has no parent , or 'subpage' otherwise
*
* @ return string
*/
public function getParentType ()
{
return $this -> ParentID == 0 ? 'root' : 'subpage' ;
}
/**
* Clear the permissions cache for SiteTree
*/
public static function reset ()
{
2017-05-12 02:47:46 +02:00
$permissions = static :: getPermissionChecker ();
if ( $permissions instanceof InheritedPermissions ) {
$permissions -> clearCache ();
}
2017-01-25 21:59:25 +01:00
}
2017-06-21 06:29:40 +02:00
2017-11-30 03:56:16 +01:00
/**
* Clear the creatableChildren cache on flush
*/
public static function flush ()
{
Injector :: inst () -> get ( CacheInterface :: class . '.SiteTree_CreatableChildren' )
-> clear ();
}
2017-06-21 06:29:40 +02:00
/**
* Update dependant pages
*/
protected function updateDependentPages ()
{
2018-04-06 05:53:57 +02:00
// Skip live stage
if ( Versioned :: get_stage () === Versioned :: LIVE ) {
return ;
}
2017-06-21 06:29:40 +02:00
// Need to flush cache to avoid outdated versionnumber references
$this -> flushCache ();
// Need to mark pages depending to this one as broken
2018-04-06 05:53:57 +02:00
/** @var Page $page */
foreach ( $this -> DependentPages () as $page ) {
// Update sync link tracking
$page -> syncLinkTracking ();
if ( $page -> isChanged ()) {
2017-06-21 06:29:40 +02:00
$page -> write ();
}
}
}
2017-11-30 03:56:16 +01:00
2017-12-13 03:36:35 +01:00
/**
* Cache key for creatableChildren () method
*
* @ param int $memberID
* @ return string
*/
2017-11-30 03:56:16 +01:00
protected function generateChildrenCacheKey ( $memberID )
{
return md5 ( $memberID . '_' . __CLASS__ );
}
2018-01-24 05:29:10 +01:00
/**
* Get the list of excluded root URL segments
*
* @ return array List of lowercase urlsegments
*/
protected function getExcludedURLSegments ()
{
$excludes = [];
// Build from rules
foreach ( Director :: config () -> get ( 'rules' ) as $pattern => $rule ) {
2022-04-13 07:07:59 +02:00
$route = explode ( '/' , $pattern ? ? '' );
if ( ! empty ( $route ) && strpos ( $route [ 0 ] ? ? '' , '$' ) === false ) {
$excludes [] = strtolower ( $route [ 0 ] ? ? '' );
2018-01-24 05:29:10 +01:00
}
}
// Build from base folders
foreach ( glob ( Director :: publicFolder () . '/*' , GLOB_ONLYDIR ) as $folder ) {
2022-04-13 07:07:59 +02:00
$excludes [] = strtolower ( basename ( $folder ? ? '' ));
2018-01-24 05:29:10 +01:00
}
$this -> extend ( 'updateExcludedURLSegments' , $excludes );
return $excludes ;
}
2018-07-13 01:37:57 +02:00
/**
* @ return array
*/
public function getAnchorsOnPage ()
{
$parseSuccess = preg_match_all (
" / \\ s+(name|id) \\ s*= \\ s*([ \" '])([^ \\ 2 \\ s>]*?) \\ 2| \\ s+(name|id) \\ s*= \\ s*([^ \" ']+)[ \\ s +>]/im " ,
2022-04-13 07:07:59 +02:00
$this -> Content ? ? '' ,
2018-07-13 01:37:57 +02:00
$matches
);
2021-09-28 21:36:37 +02:00
$anchors = [];
if ( $parseSuccess >= 1 ) {
$anchors = array_values ( array_unique ( array_filter (
array_merge ( $matches [ 3 ], $matches [ 5 ])
)));
2018-07-13 01:37:57 +02:00
}
$this -> extend ( 'updateAnchorsOnPage' , $anchors );
return $anchors ;
}
2019-08-16 05:25:21 +02:00
/**
* Returns whether this is the home page or not
*
* @ return bool
*/
public function isHomePage () : bool
{
return $this -> URLSegment === RootURLController :: get_homepage_link ();
}
2012-09-25 05:31:42 +02:00
}