2022-06-17 17:51:59 +02:00
< ? php
namespace Kirby\Cms ;
2022-12-19 14:56:05 +01:00
use Closure ;
2025-04-21 18:57:21 +02:00
use Kirby\Content\Field ;
2022-06-17 17:51:59 +02:00
use Kirby\Exception\Exception ;
use Kirby\Exception\InvalidArgumentException ;
use Kirby\Exception\NotFoundException ;
use Kirby\Filesystem\Dir ;
2022-08-31 15:02:43 +02:00
use Kirby\Http\Response ;
2022-06-17 17:51:59 +02:00
use Kirby\Http\Uri ;
use Kirby\Panel\Page as Panel ;
2025-04-21 18:57:21 +02:00
use Kirby\Template\Template ;
2022-06-17 17:51:59 +02:00
use Kirby\Toolkit\A ;
2025-04-21 18:57:21 +02:00
use Kirby\Toolkit\LazyValue ;
use Kirby\Toolkit\Str ;
use Throwable ;
2022-06-17 17:51:59 +02:00
/**
* The `$page` object is the heart and
* soul of Kirby . It is used to construct
* pages and all their dependencies like
* children , files , content , etc .
*
* @ package Kirby Cms
* @ author Bastian Allgeier < bastian @ getkirby . com >
* @ link https :// getkirby . com
* @ copyright Bastian Allgeier
* @ license https :// getkirby . com / license
*/
class Page extends ModelWithContent
{
2022-08-31 15:02:43 +02:00
use HasChildren ;
use HasFiles ;
use HasMethods ;
use HasSiblings ;
2025-04-21 18:57:21 +02:00
use PageActions ;
use PageSiblings ;
2022-08-31 15:02:43 +02:00
public const CLASS_ALIAS = 'page' ;
/**
* All registered page methods
2025-04-21 18:57:21 +02:00
* @ todo Remove when support for PHP 8.2 is dropped
2022-08-31 15:02:43 +02:00
*/
2025-04-21 18:57:21 +02:00
public static array $methods = [];
2022-08-31 15:02:43 +02:00
/**
* Registry with all Page models
*/
2025-04-21 18:57:21 +02:00
public static array $models = [];
2022-08-31 15:02:43 +02:00
/**
* The PageBlueprint object
*/
2025-04-21 18:57:21 +02:00
protected PageBlueprint | null $blueprint = null ;
2022-08-31 15:02:43 +02:00
/**
* Nesting level
*/
2025-04-21 18:57:21 +02:00
protected int $depth ;
2022-08-31 15:02:43 +02:00
/**
* Sorting number + slug
*/
2025-04-21 18:57:21 +02:00
protected string | null $dirname ;
2022-08-31 15:02:43 +02:00
/**
* Path of dirnames
*/
2025-04-21 18:57:21 +02:00
protected string | null $diruri = null ;
2022-08-31 15:02:43 +02:00
/**
* Draft status flag
*/
2025-04-21 18:57:21 +02:00
protected bool $isDraft ;
2022-08-31 15:02:43 +02:00
/**
* The Page id
*/
2025-04-21 18:57:21 +02:00
protected string | null $id = null ;
2022-08-31 15:02:43 +02:00
/**
* The template , that should be loaded
* if it exists
*/
2025-04-21 18:57:21 +02:00
protected Template | null $intendedTemplate = null ;
2022-08-31 15:02:43 +02:00
2025-04-21 18:57:21 +02:00
protected array | null $inventory = null ;
2022-08-31 15:02:43 +02:00
/**
* The sorting number
*/
2025-04-21 18:57:21 +02:00
protected int | null $num ;
2022-08-31 15:02:43 +02:00
/**
* The parent page
*/
2025-04-21 18:57:21 +02:00
protected Page | null $parent ;
2022-08-31 15:02:43 +02:00
/**
* Absolute path to the page directory
*/
2025-04-21 18:57:21 +02:00
protected string | null $root ;
2022-08-31 15:02:43 +02:00
/**
* The URL - appendix aka slug
*/
2025-04-21 18:57:21 +02:00
protected string $slug ;
2022-08-31 15:02:43 +02:00
/**
* The intended page template
*/
2025-04-21 18:57:21 +02:00
protected Template | null $template = null ;
2022-08-31 15:02:43 +02:00
/**
* The page url
*/
2025-04-21 18:57:21 +02:00
protected string | null $url ;
/**
* Creates a new page object
*/
public function __construct ( array $props )
{
if ( isset ( $props [ 'slug' ]) === false ) {
throw new InvalidArgumentException ( 'The page slug is required' );
}
parent :: __construct ( $props );
$this -> slug = $props [ 'slug' ];
// Sets the dirname manually, which works
// more reliable in connection with the inventory
// than computing the dirname afterwards
$this -> dirname = $props [ 'dirname' ] ? ? null ;
$this -> isDraft = $props [ 'isDraft' ] ? ? false ;
$this -> num = $props [ 'num' ] ? ? null ;
$this -> parent = $props [ 'parent' ] ? ? null ;
$this -> root = $props [ 'root' ] ? ? null ;
$this -> setBlueprint ( $props [ 'blueprint' ] ? ? null );
$this -> setChildren ( $props [ 'children' ] ? ? null );
$this -> setDrafts ( $props [ 'drafts' ] ? ? null );
$this -> setFiles ( $props [ 'files' ] ? ? null );
$this -> setTemplate ( $props [ 'template' ] ? ? null );
$this -> setUrl ( $props [ 'url' ] ? ? null );
}
2022-08-31 15:02:43 +02:00
/**
* Magic caller
*/
2025-04-21 18:57:21 +02:00
public function __call ( string $method , array $arguments = []) : mixed
2022-08-31 15:02:43 +02:00
{
// public property access
if ( isset ( $this -> $method ) === true ) {
return $this -> $method ;
}
// page methods
if ( $this -> hasMethod ( $method )) {
return $this -> callMethod ( $method , $arguments );
}
// return page content otherwise
return $this -> content () -> get ( $method );
}
/**
* Improved `var_dump` output
2025-04-21 18:57:21 +02:00
* @ codeCoverageIgnore
2022-08-31 15:02:43 +02:00
*/
public function __debugInfo () : array
{
return array_merge ( $this -> toArray (), [
'content' => $this -> content (),
'children' => $this -> children (),
'siblings' => $this -> siblings (),
'translations' => $this -> translations (),
'files' => $this -> files (),
]);
}
/**
* Returns the url to the api endpoint
* @ internal
*/
public function apiUrl ( bool $relative = false ) : string
{
if ( $relative === true ) {
return 'pages/' . $this -> panel () -> id ();
}
2022-12-19 14:56:05 +01:00
return $this -> kirby () -> url ( 'api' ) . '/pages/' . $this -> panel () -> id ();
2022-08-31 15:02:43 +02:00
}
/**
* Returns the blueprint object
*/
2025-04-21 18:57:21 +02:00
public function blueprint () : PageBlueprint
2022-08-31 15:02:43 +02:00
{
2025-04-21 18:57:21 +02:00
return $this -> blueprint ? ? = PageBlueprint :: factory (
'pages/' . $this -> intendedTemplate (),
'pages/default' ,
$this
);
2022-08-31 15:02:43 +02:00
}
/**
* Returns an array with all blueprints that are available for the page
*/
2022-12-19 14:56:05 +01:00
public function blueprints ( string | null $inSection = null ) : array
2022-08-31 15:02:43 +02:00
{
if ( $inSection !== null ) {
return $this -> blueprint () -> section ( $inSection ) -> blueprints ();
}
2025-04-21 18:57:21 +02:00
if ( $this -> blueprints !== null ) {
return $this -> blueprints ;
}
2022-08-31 15:02:43 +02:00
$blueprints = [];
$templates = $this -> blueprint () -> changeTemplate () ? ? $this -> blueprint () -> options ()[ 'changeTemplate' ] ? ? [];
$currentTemplate = $this -> intendedTemplate () -> name ();
if ( is_array ( $templates ) === false ) {
$templates = [];
}
// add the current template to the array if it's not already there
if ( in_array ( $currentTemplate , $templates ) === false ) {
array_unshift ( $templates , $currentTemplate );
}
// make sure every template is only included once
$templates = array_unique ( $templates );
foreach ( $templates as $template ) {
try {
$props = Blueprint :: load ( 'pages/' . $template );
$blueprints [] = [
'name' => basename ( $props [ 'name' ]),
'title' => $props [ 'title' ],
];
2022-12-19 14:56:05 +01:00
} catch ( Exception ) {
2022-08-31 15:02:43 +02:00
// skip invalid blueprints
}
}
2025-04-21 18:57:21 +02:00
return $this -> blueprints = array_values ( $blueprints );
2022-08-31 15:02:43 +02:00
}
/**
* Builds the cache id for the page
*/
protected function cacheId ( string $contentType ) : string
{
$cacheId = [ $this -> id ()];
if ( $this -> kirby () -> multilang () === true ) {
$cacheId [] = $this -> kirby () -> language () -> code ();
}
$cacheId [] = $contentType ;
return implode ( '.' , $cacheId );
}
/**
* Prepares the content for the write method
* @ internal
*/
2025-04-21 18:57:21 +02:00
public function contentFileData (
array $data ,
string | null $languageCode = null
) : array {
2022-08-31 15:02:43 +02:00
return A :: prepend ( $data , [
'title' => $data [ 'title' ] ? ? null ,
'slug' => $data [ 'slug' ] ? ? null
]);
}
/**
* Returns the content text file
* which is found by the inventory method
* @ internal
2025-04-21 18:57:21 +02:00
* @ deprecated 4.0 . 0
* @ todo Remove in v5
* @ codeCoverageIgnore
2022-08-31 15:02:43 +02:00
*/
2022-12-19 14:56:05 +01:00
public function contentFileName ( string | null $languageCode = null ) : string
2022-08-31 15:02:43 +02:00
{
2025-04-21 18:57:21 +02:00
Helpers :: deprecated ( 'The internal $model->contentFileName() method has been deprecated. Please let us know via a GitHub issue if you need this method and tell us your use case.' , 'model-content-file' );
2022-08-31 15:02:43 +02:00
return $this -> intendedTemplate () -> name ();
}
/**
* Call the page controller
* @ internal
2025-04-21 18:57:21 +02:00
*
2022-08-31 15:02:43 +02:00
* @ throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby` , `site` , `pages` or `page`
*/
2025-04-21 18:57:21 +02:00
public function controller (
array $data = [],
string $contentType = 'html'
) : array {
2022-08-31 15:02:43 +02:00
// create the template data
$data = array_merge ( $data , [
'kirby' => $kirby = $this -> kirby (),
'site' => $site = $this -> site (),
2025-04-21 18:57:21 +02:00
'pages' => new LazyValue ( fn () => $site -> children ()),
'page' => new LazyValue ( fn () => $site -> visit ( $this ))
2022-08-31 15:02:43 +02:00
]);
// call the template controller if there's one.
2025-04-21 18:57:21 +02:00
$controllerData = $kirby -> controller (
$this -> template () -> name (),
$data ,
$contentType
);
2022-08-31 15:02:43 +02:00
// merge controller data with original data safely
2025-04-21 18:57:21 +02:00
// to provide original data to template even if
// it wasn't returned by the controller explicitly
2022-08-31 15:02:43 +02:00
if ( empty ( $controllerData ) === false ) {
$classes = [
2025-04-21 18:57:21 +02:00
'kirby' => App :: class ,
'site' => Site :: class ,
'pages' => Pages :: class ,
'page' => Page :: class
2022-08-31 15:02:43 +02:00
];
foreach ( $controllerData as $key => $value ) {
2025-04-21 18:57:21 +02:00
$data [ $key ] = match ( true ) {
// original data wasn't overwritten
array_key_exists ( $key , $classes ) === false => $value ,
// original data was overwritten, but matches expected type
$value instanceof $classes [ $key ] => $value ,
// throw error if data was overwritten with wrong type
default => throw new InvalidArgumentException ( 'The returned variable "' . $key . '" from the controller "' . $this -> template () -> name () . '" is not of the required type "' . $classes [ $key ] . '"' )
};
2022-08-31 15:02:43 +02:00
}
}
2025-04-21 18:57:21 +02:00
// unwrap remaining lazy values in data
// (happens if the controller didn't override an original lazy Kirby object)
$data = LazyValue :: unwrap ( $data );
2022-08-31 15:02:43 +02:00
return $data ;
}
/**
* Returns a number indicating how deep the page
* is nested within the content folder
*/
public function depth () : int
{
return $this -> depth ? ? = ( substr_count ( $this -> id (), '/' ) + 1 );
}
/**
2025-07-04 15:08:52 +02:00
* Returns the directory name ( UID with optional sorting number )
2022-08-31 15:02:43 +02:00
*/
public function dirname () : string
{
if ( $this -> dirname !== null ) {
return $this -> dirname ;
}
if ( $this -> num () !== null ) {
return $this -> dirname = $this -> num () . Dir :: $numSeparator . $this -> uid ();
}
2022-12-19 14:56:05 +01:00
return $this -> dirname = $this -> uid ();
2022-08-31 15:02:43 +02:00
}
/**
2025-07-04 15:08:52 +02:00
* Returns the directory path relative to the `content` root
* ( including optional sorting numbers and draft directories )
2022-08-31 15:02:43 +02:00
*/
public function diruri () : string
{
if ( is_string ( $this -> diruri ) === true ) {
return $this -> diruri ;
}
if ( $this -> isDraft () === true ) {
$dirname = '_drafts/' . $this -> dirname ();
} else {
$dirname = $this -> dirname ();
}
if ( $parent = $this -> parent ()) {
return $this -> diruri = $parent -> diruri () . '/' . $dirname ;
}
2022-12-19 14:56:05 +01:00
return $this -> diruri = $dirname ;
2022-08-31 15:02:43 +02:00
}
/**
* Checks if the page exists on disk
*/
public function exists () : bool
{
return is_dir ( $this -> root ()) === true ;
}
/**
* Constructs a Page object and also
* takes page models into account .
* @ internal
*/
2025-04-21 18:57:21 +02:00
public static function factory ( $props ) : static
2022-08-31 15:02:43 +02:00
{
2025-04-21 18:57:21 +02:00
return static :: model ( $props [ 'model' ] ? ? 'default' , $props );
2022-08-31 15:02:43 +02:00
}
/**
* Redirects to this page ,
* wrapper for the `go()` helper
*
* @ since 3.4 . 0
*
* @ param array $options Options for `Kirby\Http\Uri` to create URL parts
* @ param int $code HTTP status code
*/
2025-04-21 18:57:21 +02:00
public function go ( array $options = [], int $code = 302 ) : void
2022-08-31 15:02:43 +02:00
{
Response :: go ( $this -> url ( $options ), $code );
}
/**
* Checks if the intended template
* for the page exists .
*/
public function hasTemplate () : bool
{
return $this -> intendedTemplate () === $this -> template ();
}
/**
* Returns the Page Id
*/
public function id () : string
{
if ( $this -> id !== null ) {
return $this -> id ;
}
// set the id, depending on the parent
if ( $parent = $this -> parent ()) {
return $this -> id = $parent -> id () . '/' . $this -> uid ();
}
return $this -> id = $this -> uid ();
}
/**
* Returns the template that should be
* loaded if it exists .
*/
2025-04-21 18:57:21 +02:00
public function intendedTemplate () : Template
2022-08-31 15:02:43 +02:00
{
if ( $this -> intendedTemplate !== null ) {
return $this -> intendedTemplate ;
}
return $this -> setTemplate ( $this -> inventory ()[ 'template' ]) -> intendedTemplate ();
}
/**
* Returns the inventory of files
* children and content files
* @ internal
*/
public function inventory () : array
{
if ( $this -> inventory !== null ) {
return $this -> inventory ;
}
$kirby = $this -> kirby ();
return $this -> inventory = Dir :: inventory (
$this -> root (),
$kirby -> contentExtension (),
$kirby -> contentIgnore (),
$kirby -> multilang ()
);
}
/**
* Compares the current object with the given page object
*
* @ param \Kirby\Cms\Page | string $page
*/
public function is ( $page ) : bool
{
2022-12-19 14:56:05 +01:00
if ( $page instanceof self === false ) {
2022-08-31 15:02:43 +02:00
if ( is_string ( $page ) === false ) {
return false ;
}
$page = $this -> kirby () -> page ( $page );
}
2022-12-19 14:56:05 +01:00
if ( $page instanceof self === false ) {
2022-08-31 15:02:43 +02:00
return false ;
}
return $this -> id () === $page -> id ();
}
/**
2025-04-21 18:57:21 +02:00
* Checks if the page is accessible that accessible and listable .
* This permission depends on the `read` option until v5
2022-08-31 15:02:43 +02:00
*/
2025-04-21 18:57:21 +02:00
public function isAccessible () : bool
2022-08-31 15:02:43 +02:00
{
2025-04-21 18:57:21 +02:00
// TODO: remove this check when `read` option deprecated in v5
if ( $this -> isReadable () === false ) {
return false ;
2022-08-31 15:02:43 +02:00
}
2025-04-21 18:57:21 +02:00
static $accessible = [];
$role = $this -> kirby () -> user () ? -> role () -> id () ? ? '__none__' ;
$template = $this -> intendedTemplate () -> name ();
$accessible [ $role ] ? ? = [];
return $accessible [ $role ][ $template ] ? ? = $this -> permissions () -> can ( 'access' );
2022-08-31 15:02:43 +02:00
}
/**
2025-04-21 18:57:21 +02:00
* Checks if the page is the current page
*/
public function isActive () : bool
{
return $this -> site () -> page () ? -> is ( $this ) === true ;
}
/**
* Checks if the page is a direct or indirect ancestor
* of the given $page object
2022-08-31 15:02:43 +02:00
*/
public function isAncestorOf ( Page $child ) : bool
{
return $child -> parents () -> has ( $this -> id ()) === true ;
}
/**
* Checks if the page can be cached in the
* pages cache . This will also check if one
* of the ignore rules from the config kick in .
*/
public function isCacheable () : bool
{
$kirby = $this -> kirby ();
$cache = $kirby -> cache ( 'pages' );
$options = $cache -> options ();
$ignore = $options [ 'ignore' ] ? ? null ;
// the pages cache is switched off
if (( $options [ 'active' ] ? ? false ) === false ) {
return false ;
}
// inspect the current request
$request = $kirby -> request ();
// disable the pages cache for any request types but GET or HEAD
if ( in_array ( $request -> method (), [ 'GET' , 'HEAD' ]) === false ) {
return false ;
}
// disable the pages cache when there's request data
if ( empty ( $request -> data ()) === false ) {
return false ;
}
// disable the pages cache when there are any params
if ( $request -> params () -> isNotEmpty ()) {
return false ;
}
// check for a custom ignore rule
2022-12-19 14:56:05 +01:00
if ( $ignore instanceof Closure ) {
2022-08-31 15:02:43 +02:00
if ( $ignore ( $this ) === true ) {
return false ;
}
}
// ignore pages by id
if ( is_array ( $ignore ) === true ) {
if ( in_array ( $this -> id (), $ignore ) === true ) {
return false ;
}
}
return true ;
}
/**
* Checks if the page is a child of the given page
*
* @ param \Kirby\Cms\Page | string $parent
*/
public function isChildOf ( $parent ) : bool
{
2022-12-19 14:56:05 +01:00
return $this -> parent () ? -> is ( $parent ) ? ? false ;
2022-08-31 15:02:43 +02:00
}
/**
* Checks if the page is a descendant of the given page
*
* @ param \Kirby\Cms\Page | string $parent
*/
public function isDescendantOf ( $parent ) : bool
{
if ( is_string ( $parent ) === true ) {
$parent = $this -> site () -> find ( $parent );
}
if ( ! $parent ) {
return false ;
}
return $this -> parents () -> has ( $parent -> id ()) === true ;
}
/**
* Checks if the page is a descendant of the currently active page
*/
public function isDescendantOfActive () : bool
{
if ( $active = $this -> site () -> page ()) {
return $this -> isDescendantOf ( $active );
}
return false ;
}
/**
* Checks if the current page is a draft
*/
public function isDraft () : bool
{
return $this -> isDraft ;
}
/**
* Checks if the page is the error page
*/
public function isErrorPage () : bool
{
return $this -> id () === $this -> site () -> errorPageId ();
}
/**
* Checks if the page is the home page
*/
public function isHomePage () : bool
{
return $this -> id () === $this -> site () -> homePageId ();
}
/**
* It ' s often required to check for the
* home and error page to stop certain
* actions . That 's why there' s a shortcut .
*/
public function isHomeOrErrorPage () : bool
{
return $this -> isHomePage () === true || $this -> isErrorPage () === true ;
}
2025-04-21 18:57:21 +02:00
/**
* Check if the page can be listable by the current user
* This permission depends on the `read` option until v5
*/
public function isListable () : bool
{
// TODO: remove this check when `read` option deprecated in v5
if ( $this -> isReadable () === false ) {
return false ;
}
// not accessible also means not listable
if ( $this -> isAccessible () === false ) {
return false ;
}
static $listable = [];
$role = $this -> kirby () -> user () ? -> role () -> id () ? ? '__none__' ;
$template = $this -> intendedTemplate () -> name ();
$listable [ $role ] ? ? = [];
return $listable [ $role ][ $template ] ? ? = $this -> permissions () -> can ( 'list' );
}
2022-08-31 15:02:43 +02:00
/**
* Checks if the page has a sorting number
*/
public function isListed () : bool
{
2024-12-20 12:37:52 +01:00
return $this -> isPublished () && $this -> num () !== null ;
2022-08-31 15:02:43 +02:00
}
2025-04-21 18:57:21 +02:00
public function isMovableTo ( Page | Site $parent ) : bool
{
try {
return PageRules :: move ( $this , $parent );
} catch ( Throwable ) {
return false ;
}
}
2022-08-31 15:02:43 +02:00
/**
* Checks if the page is open .
* Open pages are either the current one
* or descendants of the current one .
*/
public function isOpen () : bool
{
if ( $this -> isActive () === true ) {
return true ;
}
2022-12-19 14:56:05 +01:00
if ( $this -> site () -> page () ? -> parents () -> has ( $this -> id ()) === true ) {
return true ;
2022-08-31 15:02:43 +02:00
}
return false ;
}
/**
* Checks if the page is not a draft .
*/
public function isPublished () : bool
{
return $this -> isDraft () === false ;
}
/**
* Check if the page can be read by the current user
2025-04-21 18:57:21 +02:00
* @ todo Deprecate `read` option in v5 and make the necessary changes for `access` and `list` options .
2022-08-31 15:02:43 +02:00
*/
public function isReadable () : bool
{
2025-04-21 18:57:21 +02:00
static $readable = [];
$role = $this -> kirby () -> user () ? -> role () -> id () ? ? '__none__' ;
$template = $this -> intendedTemplate () -> name ();
$readable [ $role ] ? ? = [];
2022-08-31 15:02:43 +02:00
2025-04-21 18:57:21 +02:00
return $readable [ $role ][ $template ] ? ? = $this -> permissions () -> can ( 'read' );
2022-08-31 15:02:43 +02:00
}
/**
* Checks if the page is sortable
*/
public function isSortable () : bool
{
return $this -> permissions () -> can ( 'sort' );
}
/**
* Checks if the page has no sorting number
*/
public function isUnlisted () : bool
{
2024-12-20 12:37:52 +01:00
return $this -> isPublished () && $this -> num () === null ;
2022-08-31 15:02:43 +02:00
}
/**
* Checks if the page access is verified .
* This is only used for drafts so far .
* @ internal
*/
2025-04-21 18:57:21 +02:00
public function isVerified ( string | null $token = null ) : bool
2022-08-31 15:02:43 +02:00
{
if (
2024-12-20 12:37:52 +01:00
$this -> isPublished () === true &&
2022-08-31 15:02:43 +02:00
$this -> parents () -> findBy ( 'status' , 'draft' ) === null
) {
return true ;
}
if ( $token === null ) {
return false ;
}
return $this -> token () === $token ;
}
/**
* Returns the root to the media folder for the page
* @ internal
*/
public function mediaRoot () : string
{
return $this -> kirby () -> root ( 'media' ) . '/pages/' . $this -> id ();
}
/**
* The page ' s base URL for any files
* @ internal
*/
public function mediaUrl () : string
{
return $this -> kirby () -> url ( 'media' ) . '/pages/' . $this -> id ();
}
/**
* Creates a page model if it has been registered
* @ internal
*/
2025-04-21 18:57:21 +02:00
public static function model ( string $name , array $props = []) : static
2022-08-31 15:02:43 +02:00
{
2025-04-21 18:57:21 +02:00
$class = static :: $models [ $name ] ? ? null ;
$class ? ? = static :: $models [ 'default' ] ? ? null ;
if ( $class !== null ) {
2022-08-31 15:02:43 +02:00
$object = new $class ( $props );
2022-12-19 14:56:05 +01:00
if ( $object instanceof self ) {
2022-08-31 15:02:43 +02:00
return $object ;
}
}
return new static ( $props );
}
/**
* Returns the last modification date of the page
*/
2025-04-21 18:57:21 +02:00
public function modified (
string | null $format = null ,
string | null $handler = null ,
string | null $languageCode = null
) : int | string | false | null {
$identifier = $this -> isDraft () === true ? 'changes' : 'published' ;
$modified = $this -> storage () -> modified (
$identifier ,
$languageCode
2022-08-31 15:02:43 +02:00
);
2025-04-21 18:57:21 +02:00
if ( $modified === null ) {
return null ;
}
return Str :: date ( $modified , $format , $handler );
2022-08-31 15:02:43 +02:00
}
/**
* Returns the sorting number
*/
2022-12-19 14:56:05 +01:00
public function num () : int | null
2022-08-31 15:02:43 +02:00
{
return $this -> num ;
}
/**
* Returns the panel info object
*/
2025-04-21 18:57:21 +02:00
public function panel () : Panel
2022-08-31 15:02:43 +02:00
{
return new Panel ( $this );
}
/**
* Returns the parent Page object
*/
2025-04-21 18:57:21 +02:00
public function parent () : Page | null
2022-08-31 15:02:43 +02:00
{
return $this -> parent ;
}
/**
* Returns the parent id , if a parent exists
* @ internal
*/
2022-12-19 14:56:05 +01:00
public function parentId () : string | null
2022-08-31 15:02:43 +02:00
{
2022-12-19 14:56:05 +01:00
return $this -> parent () ? -> id ();
2022-08-31 15:02:43 +02:00
}
/**
* Returns the parent model ,
* which can either be another Page
* or the Site
* @ internal
*/
2025-04-21 18:57:21 +02:00
public function parentModel () : Page | Site
2022-08-31 15:02:43 +02:00
{
return $this -> parent () ? ? $this -> site ();
}
/**
* Returns a list of all parents and their parents recursively
*/
2025-04-21 18:57:21 +02:00
public function parents () : Pages
2022-08-31 15:02:43 +02:00
{
$parents = new Pages ();
$page = $this -> parent ();
while ( $page !== null ) {
$parents -> append ( $page -> id (), $page );
$page = $page -> parent ();
}
return $parents ;
}
2022-12-19 14:56:05 +01:00
/**
* Return the permanent URL to the page using its UUID
* @ since 3.8 . 0
*/
public function permalink () : string | null
{
return $this -> uuid () ? -> url ();
}
2022-08-31 15:02:43 +02:00
/**
* Returns the permissions object for this page
*/
2025-04-21 18:57:21 +02:00
public function permissions () : PagePermissions
2022-08-31 15:02:43 +02:00
{
return new PagePermissions ( $this );
}
/**
* Draft preview Url
* @ internal
*/
2022-12-19 14:56:05 +01:00
public function previewUrl () : string | null
2022-08-31 15:02:43 +02:00
{
$preview = $this -> blueprint () -> preview ();
if ( $preview === false ) {
return null ;
}
2025-04-21 18:57:21 +02:00
$url = match ( $preview ) {
true => $this -> url (),
default => $preview
};
2022-08-31 15:02:43 +02:00
if ( $this -> isDraft () === true ) {
$uri = new Uri ( $url );
$uri -> query -> token = $this -> token ();
$url = $uri -> toString ();
}
return $url ;
}
/**
* Renders the page with the given data .
*
* An optional content type can be passed to
* render a content representation instead of
* the default template .
*
* @ param string $contentType
* @ throws \Kirby\Exception\NotFoundException If the default template cannot be found
*/
public function render ( array $data = [], $contentType = 'html' ) : string
{
$kirby = $this -> kirby ();
$cache = $cacheId = $html = null ;
// try to get the page from cache
if ( empty ( $data ) === true && $this -> isCacheable () === true ) {
$cache = $kirby -> cache ( 'pages' );
$cacheId = $this -> cacheId ( $contentType );
$result = $cache -> get ( $cacheId );
$html = $result [ 'html' ] ? ? null ;
$response = $result [ 'response' ] ? ? [];
$usesAuth = $result [ 'usesAuth' ] ? ? false ;
$usesCookies = $result [ 'usesCookies' ] ? ? [];
// if the request contains dynamic data that the cached response
// relied on, don't use the cache to allow dynamic code to run
if ( Responder :: isPrivate ( $usesAuth , $usesCookies ) === true ) {
$html = null ;
}
// reconstruct the response configuration
if ( empty ( $html ) === false && empty ( $response ) === false ) {
$kirby -> response () -> fromArray ( $response );
}
}
// fetch the page regularly
if ( $html === null ) {
if ( $contentType === 'html' ) {
$template = $this -> template ();
} else {
$template = $this -> representation ( $contentType );
}
if ( $template -> exists () === false ) {
throw new NotFoundException ([
'key' => 'template.default.notFound'
]);
}
$kirby -> data = $this -> controller ( $data , $contentType );
2022-12-19 14:56:05 +01:00
// trigger before hook and apply for `data`
$kirby -> data = $kirby -> apply ( 'page.render:before' , [
'contentType' => $contentType ,
'data' => $kirby -> data ,
'page' => $this
], 'data' );
2022-08-31 15:02:43 +02:00
// render the page
$html = $template -> render ( $kirby -> data );
2022-12-19 14:56:05 +01:00
// trigger after hook and apply for `html`
$html = $kirby -> apply ( 'page.render:after' , [
'contentType' => $contentType ,
'data' => $kirby -> data ,
'html' => $html ,
'page' => $this
], 'html' );
2022-08-31 15:02:43 +02:00
// cache the result
$response = $kirby -> response ();
if ( $cache !== null && $response -> cache () === true ) {
$cache -> set ( $cacheId , [
'html' => $html ,
'response' => $response -> toArray (),
'usesAuth' => $response -> usesAuth (),
'usesCookies' => $response -> usesCookies (),
], $response -> expires () ? ? 0 );
}
}
return $html ;
}
/**
* @ internal
* @ throws \Kirby\Exception\NotFoundException If the content representation cannot be found
*/
2025-04-21 18:57:21 +02:00
public function representation ( mixed $type ) : Template
2022-08-31 15:02:43 +02:00
{
$kirby = $this -> kirby ();
$template = $this -> template ();
$representation = $kirby -> template ( $template -> name (), $type );
if ( $representation -> exists () === true ) {
return $representation ;
}
throw new NotFoundException ( 'The content representation cannot be found' );
}
/**
* Returns the absolute root to the page directory
* No matter if it exists or not .
*/
public function root () : string
{
return $this -> root ? ? = $this -> kirby () -> root ( 'content' ) . '/' . $this -> diruri ();
}
/**
* Returns the PageRules class instance
* which is being used in various methods
* to check for valid actions and input .
*/
2025-04-21 18:57:21 +02:00
protected function rules () : PageRules
2022-08-31 15:02:43 +02:00
{
return new PageRules ();
}
/**
* Search all pages within the current page
*/
2025-04-21 18:57:21 +02:00
public function search ( string | null $query = null , string | array $params = []) : Pages
2022-08-31 15:02:43 +02:00
{
return $this -> index () -> search ( $query , $params );
}
/**
* Sets the Blueprint object
*
* @ return $this
*/
2025-04-21 18:57:21 +02:00
protected function setBlueprint ( array | null $blueprint = null ) : static
2022-08-31 15:02:43 +02:00
{
if ( $blueprint !== null ) {
$blueprint [ 'model' ] = $this ;
$this -> blueprint = new PageBlueprint ( $blueprint );
}
return $this ;
}
/**
* Sets the intended template
*
* @ return $this
*/
2025-04-21 18:57:21 +02:00
protected function setTemplate ( string | null $template = null ) : static
2022-08-31 15:02:43 +02:00
{
if ( $template !== null ) {
$this -> intendedTemplate = $this -> kirby () -> template ( $template );
}
return $this ;
}
/**
* Sets the Url
*
* @ return $this
*/
2025-04-21 18:57:21 +02:00
protected function setUrl ( string | null $url = null ) : static
2022-08-31 15:02:43 +02:00
{
if ( is_string ( $url ) === true ) {
$url = rtrim ( $url , '/' );
}
$this -> url = $url ;
return $this ;
}
/**
* Returns the slug of the page
*/
2025-04-21 18:57:21 +02:00
public function slug ( string | null $languageCode = null ) : string
2022-08-31 15:02:43 +02:00
{
if ( $this -> kirby () -> multilang () === true ) {
2023-04-14 16:34:06 +02:00
$languageCode ? ? = $this -> kirby () -> languageCode ();
2022-08-31 15:02:43 +02:00
$defaultLanguageCode = $this -> kirby () -> defaultLanguage () -> code ();
2023-04-14 16:34:06 +02:00
if (
$languageCode !== $defaultLanguageCode &&
$translation = $this -> translations () -> find ( $languageCode )
) {
2022-08-31 15:02:43 +02:00
return $translation -> slug () ? ? $this -> slug ;
}
}
return $this -> slug ;
}
/**
* Returns the page status , which
* can be `draft` , `listed` or `unlisted`
*/
public function status () : string
{
if ( $this -> isDraft () === true ) {
return 'draft' ;
}
if ( $this -> isUnlisted () === true ) {
return 'unlisted' ;
}
return 'listed' ;
}
/**
* Returns the final template
*/
2025-04-21 18:57:21 +02:00
public function template () : Template
2022-08-31 15:02:43 +02:00
{
if ( $this -> template !== null ) {
return $this -> template ;
}
$intended = $this -> intendedTemplate ();
if ( $intended -> exists () === true ) {
return $this -> template = $intended ;
}
return $this -> template = $this -> kirby () -> template ( 'default' );
}
/**
* Returns the title field or the slug as fallback
*/
2025-04-21 18:57:21 +02:00
public function title () : Field
2022-08-31 15:02:43 +02:00
{
return $this -> content () -> get ( 'title' ) -> or ( $this -> slug ());
}
/**
* Converts the most important
* properties to array
*/
public function toArray () : array
{
2025-04-21 18:57:21 +02:00
return array_merge ( parent :: toArray (), [
'children' => $this -> children () -> keys (),
'files' => $this -> files () -> keys (),
'id' => $this -> id (),
'mediaUrl' => $this -> mediaUrl (),
'mediaRoot' => $this -> mediaRoot (),
'num' => $this -> num (),
'parent' => $this -> parent () ? -> id (),
'slug' => $this -> slug (),
'template' => $this -> template (),
'uid' => $this -> uid (),
'uri' => $this -> uri (),
'url' => $this -> url ()
]);
2022-08-31 15:02:43 +02:00
}
/**
* Returns a verification token , which
* is used for the draft authentication
*/
protected function token () : string
{
2025-04-21 18:57:21 +02:00
return $this -> kirby () -> contentToken (
$this ,
$this -> id () . $this -> template ()
);
2022-08-31 15:02:43 +02:00
}
/**
* Returns the UID of the page .
* The UID is basically the same as the
* slug , but stays the same on
* multi - language sites . Whereas the slug
* can be translated .
*
* @ see self :: slug ()
*/
public function uid () : string
{
return $this -> slug ;
}
/**
* The uri is the same as the id , except
* that it will be translated in multi - language setups
*/
2025-04-21 18:57:21 +02:00
public function uri ( string | null $languageCode = null ) : string
2022-08-31 15:02:43 +02:00
{
// set the id, depending on the parent
if ( $parent = $this -> parent ()) {
return $parent -> uri ( $languageCode ) . '/' . $this -> slug ( $languageCode );
}
return $this -> slug ( $languageCode );
}
/**
* Returns the Url
*
* @ param array | string | null $options
*/
public function url ( $options = null ) : string
{
if ( $this -> kirby () -> multilang () === true ) {
if ( is_string ( $options ) === true ) {
return $this -> urlForLanguage ( $options );
}
2022-12-19 14:56:05 +01:00
return $this -> urlForLanguage ( null , $options );
2022-08-31 15:02:43 +02:00
}
if ( $options !== null ) {
return Url :: to ( $this -> url (), $options );
}
if ( is_string ( $this -> url ) === true ) {
return $this -> url ;
}
if ( $this -> isHomePage () === true ) {
return $this -> url = $this -> site () -> url ();
}
if ( $parent = $this -> parent ()) {
if ( $parent -> isHomePage () === true ) {
return $this -> url = $this -> kirby () -> url ( 'base' ) . '/' . $parent -> uid () . '/' . $this -> uid ();
}
2022-12-19 14:56:05 +01:00
return $this -> url = $this -> parent () -> url () . '/' . $this -> uid ();
2022-08-31 15:02:43 +02:00
}
return $this -> url = $this -> kirby () -> url ( 'base' ) . '/' . $this -> uid ();
}
/**
* Builds the Url for a specific language
*
* @ internal
* @ param string | null $language
*/
2025-04-21 18:57:21 +02:00
public function urlForLanguage (
$language = null ,
array | null $options = null
) : string {
2022-08-31 15:02:43 +02:00
if ( $options !== null ) {
return Url :: to ( $this -> urlForLanguage ( $language ), $options );
}
if ( $this -> isHomePage () === true ) {
return $this -> url = $this -> site () -> urlForLanguage ( $language );
}
if ( $parent = $this -> parent ()) {
if ( $parent -> isHomePage () === true ) {
return $this -> url = $this -> site () -> urlForLanguage ( $language ) . '/' . $parent -> slug ( $language ) . '/' . $this -> slug ( $language );
}
2022-12-19 14:56:05 +01:00
return $this -> url = $this -> parent () -> urlForLanguage ( $language ) . '/' . $this -> slug ( $language );
2022-08-31 15:02:43 +02:00
}
return $this -> url = $this -> site () -> urlForLanguage ( $language ) . '/' . $this -> slug ( $language );
}
2022-06-17 17:51:59 +02:00
}