From 8439abbe97a2b40e7efe52d16cbe9bc31f4029f6 Mon Sep 17 00:00:00 2001
From: Diego Najar <dignajar@gmail.com>
Date: Sun, 12 Sep 2021 22:06:29 +0200
Subject: [PATCH] add comments, and styling

---
 bl-kernel/boot/init.php              |   14 +-
 bl-kernel/helpers/sanitize.class.php |   11 +-
 bl-kernel/js/variables.php           |    1 +
 bl-kernel/page.class.php             | 1307 ++++++++++++++------------
 bl-kernel/pages.class.php            |   90 +-
 bl-languages/en.json                 |    2 +
 6 files changed, 759 insertions(+), 666 deletions(-)

diff --git a/bl-kernel/boot/init.php b/bl-kernel/boot/init.php
index c02bf708..8c1eba00 100644
--- a/bl-kernel/boot/init.php
+++ b/bl-kernel/boot/init.php
@@ -219,7 +219,7 @@ define('THEME_DIR_LANG',		THEME_DIR.'languages'.DS);
 
 // --- Absolute paths with domain ---
 // This paths are absolutes for the user / web browsing.
-define('DOMAIN',			$site->domain());
+define('DOMAIN',			    $site->domain());
 define('DOMAIN_BASE',			DOMAIN.HTML_PATH_ROOT);
 define('DOMAIN_CORE_JS',		DOMAIN.HTML_PATH_CORE_JS);
 define('DOMAIN_CORE_CSS',		DOMAIN.HTML_PATH_CORE_CSS);
@@ -228,17 +228,15 @@ define('DOMAIN_THEME',			DOMAIN.HTML_PATH_THEME);
 define('DOMAIN_THEME_CSS',		DOMAIN.HTML_PATH_THEME_CSS);
 define('DOMAIN_THEME_JS',		DOMAIN.HTML_PATH_THEME_JS);
 define('DOMAIN_THEME_IMG',		DOMAIN.HTML_PATH_THEME_IMG);
-define('DOMAIN_ADMIN_THEME',		DOMAIN.HTML_PATH_ADMIN_THEME);
-define('DOMAIN_ADMIN_THEME_CSS',	DOMAIN.HTML_PATH_ADMIN_THEME_CSS);
-define('DOMAIN_ADMIN_THEME_JS',		DOMAIN.HTML_PATH_ADMIN_THEME_JS);
+define('DOMAIN_ADMIN_THEME',	DOMAIN.HTML_PATH_ADMIN_THEME);
+define('DOMAIN_ADMIN_THEME_CSS',DOMAIN.HTML_PATH_ADMIN_THEME_CSS);
+define('DOMAIN_ADMIN_THEME_JS',	DOMAIN.HTML_PATH_ADMIN_THEME_JS);
 define('DOMAIN_UPLOADS',		DOMAIN.HTML_PATH_UPLOADS);
-define('DOMAIN_UPLOADS_PAGES',		DOMAIN.HTML_PATH_UPLOADS_PAGES);
-define('DOMAIN_UPLOADS_PROFILES',	DOMAIN.HTML_PATH_UPLOADS_PROFILES);
+define('DOMAIN_UPLOADS_PAGES',	DOMAIN.HTML_PATH_UPLOADS_PAGES);
+define('DOMAIN_UPLOADS_PROFILES',DOMAIN.HTML_PATH_UPLOADS_PROFILES);
 define('DOMAIN_PLUGINS',		DOMAIN.HTML_PATH_PLUGINS);
 define('DOMAIN_CONTENT',		DOMAIN.HTML_PATH_CONTENT);
-
 define('DOMAIN_ADMIN',			DOMAIN_BASE.ADMIN_URI_FILTER.'/');
-
 define('DOMAIN_TAGS',			Text::addSlashes(DOMAIN_BASE.TAG_URI_FILTER, false, true));
 define('DOMAIN_CATEGORIES',		Text::addSlashes(DOMAIN_BASE.CATEGORY_URI_FILTER, false, true));
 define('DOMAIN_PAGES',			Text::addSlashes(DOMAIN_BASE.PAGE_URI_FILTER, false, true));
diff --git a/bl-kernel/helpers/sanitize.class.php b/bl-kernel/helpers/sanitize.class.php
index f6311c4b..3e2c6cf3 100644
--- a/bl-kernel/helpers/sanitize.class.php
+++ b/bl-kernel/helpers/sanitize.class.php
@@ -6,8 +6,15 @@ class Sanitize {
 		return strip_tags($text);
 	}
 
-	// Convert special characters to HTML entities
-	public static function html($text)
+    /**
+     * Convert special characters to HTML entities.
+     * For example, & => &amp;
+     * For example, " => &quot;
+     *
+     * @param string $text
+     * @return string
+     */
+	public static function html(string $text): string
 	{
 		$flags = ENT_COMPAT;
 
diff --git a/bl-kernel/js/variables.php b/bl-kernel/js/variables.php
index e9109637..d3072227 100644
--- a/bl-kernel/js/variables.php
+++ b/bl-kernel/js/variables.php
@@ -18,6 +18,7 @@ echo 'var DOMAIN_PAGES = "'.DOMAIN_PAGES.'";'.PHP_EOL;
 echo 'var DOMAIN_ADMIN = "'.DOMAIN_ADMIN.'";'.PHP_EOL;
 echo 'var DOMAIN_CONTENT = "'.DOMAIN_CONTENT.'";'.PHP_EOL;
 echo 'var DOMAIN_UPLOADS = "'.DOMAIN_UPLOADS.'";'.PHP_EOL;
+echo 'var DOMAIN_UPLOADS_PAGES = "'.DOMAIN_UPLOADS_PAGES.'";'.PHP_EOL;
 echo 'var DB_DATE_FORMAT = "'.DB_DATE_FORMAT.'";'.PHP_EOL;
 echo 'var AUTOSAVE_INTERVAL = "'.AUTOSAVE_INTERVAL.'";'.PHP_EOL;
 echo 'var PAGE_BREAK = "'.PAGE_BREAK.'";'.PHP_EOL;
diff --git a/bl-kernel/page.class.php b/bl-kernel/page.class.php
index d4e47297..6c6d3f23 100644
--- a/bl-kernel/page.class.php
+++ b/bl-kernel/page.class.php
@@ -2,631 +2,686 @@
 
 class Page {
 
-	protected $vars;
-
-	function __construct($key)
-	{
-		global $pages;
-
-		$this->vars['key'] = $key;
-		// If key is FALSE, the page is create with default values, like an empty page
-		// Useful for Page Not Found
-		if ($key===false) {
-			$row = $pages->getDefaultFields();
-		} else {
-			if (Text::isEmpty($key) || !$pages->exists($key)) {
-				$errorMessage = 'Page not found in database by key ['.$key.']';
-				Log::set(__METHOD__.LOG_SEP.$errorMessage);
-				throw new Exception($errorMessage);
-			}
-			$row = $pages->getPageDB($key);
-		}
-
-		foreach ($row as $field=>$value) {
-			if ($field=='date') {
-				$this->setField('dateRaw', $value);
-			} else {
-				$this->setField($field, $value);
-			}
-		}
-	}
-
-	public function getValue($field)
-	{
-		if (isset($this->vars[$field])) {
-			return $this->vars[$field];
-		}
-		return false;
-	}
-
-	public function setField($field, $value)
-	{
-		$this->vars[$field] = $value;
-		return true;
-	}
-
-	// Returns the raw content
-	// This content is not markdown parser
-	// (boolean) $sanitize, TRUE returns the content sanitized
-	public function contentRaw($sanitize=false)
-	{
-		$key = $this->key();
-		$filePath = PATH_PAGES.$key.DS.FILENAME;
-		$contentRaw = file_get_contents($filePath);
-
-		if ($sanitize) {
-			return Sanitize::html($contentRaw);
-		}
-		return $contentRaw;
-	}
-
-	// Returns the full content
-	// This content is markdown parser
-	// (boolean) $sanitize, TRUE returns the content sanitized
-	public function content($sanitize=false)
-	{
-		// If already set the content, return it
-		$content = $this->getValue('content');
-		if (!empty($content)) {
-			return $content;
-		}
-
-		// Get the raw content
-		$content = $this->contentRaw();
-
-		// Parse Markdown
-		if (MARKDOWN_PARSER) {
-			$parsedown = new Parsedown();
-			$content = $parsedown->text($content);
-		}
-
-		// Parse img src relative to absolute (with domain)
-		if (IMAGE_RELATIVE_TO_ABSOLUTE) {
-			$domain = IMAGE_RESTRICT?DOMAIN_UPLOADS_PAGES.$this->uuid().'/':DOMAIN_UPLOADS;
-			$content = Text::imgRel2Abs($content, $domain);
-		}
-
-		if ($sanitize) {
-			return Sanitize::html($content);
-		}
-		return $content;
-	}
-
-	// Returns the first part of the content if the content is splited, otherwise is returned the full content
-	// This content is markdown parser
-	// (boolean) $sanitize, TRUE returns the content sanitized
-	public function contentBreak($sanitize=false)
-	{
-		$content = $this->content($sanitize);
-		$explode = explode(PAGE_BREAK, $content);
-		return $explode[0];
-	}
-
-	// Returns the date according to locale settings and the format defined in the system
-	public function date($format=false)
-	{
-		$dateRaw = $this->dateRaw();
-		if ($format===false) {
-			global $site;
-			$format = $site->dateFormat();
-		}
-		return Date::format($dateRaw, DB_DATE_FORMAT, $format);
-	}
-
-	// Returns the date according to locale settings and format as database stored
-	public function dateRaw()
-	{
-		// This field is set in the constructor
-		return $this->getValue('dateRaw');
-	}
-
-	// Returns the date according to locale settings and format settings
-	public function dateModified($format=false)
-	{
-		$dateRaw = $this->getValue('dateModified');
-		if ($format===false) {
-			global $site;
-			$format = $site->dateFormat();
-		}
-		return Date::format($dateRaw, DB_DATE_FORMAT, $format);
-	}
-
-	// Returns the username who created the page
-	public function username()
-	{
-		return $this->getValue('username');
-	}
-
-	// TODO: Check if necessary this function
-	public function getDB()
-	{
-		return $this->vars;
-	}
-
-	// Returns the permalink
-	// (boolean) $absolute, TRUE returns the page link with the DOMAIN, FALSE without the DOMAIN
-	public function permalink($absolute=true)
-	{
-		// Get the key of the page
-		$key = $this->key();
-
-		if($absolute) {
-			return DOMAIN_PAGES.$key;
-		}
-
-		return HTML_PATH_ROOT.PAGE_URI_FILTER.$key;
-	}
-
-	public function url($absolute=true)
-	{
-		return $this->permalink($absolute);
-	}
-
-	// Returns the previous page key
-	public function previousKey()
-	{
-		global $pages;
-		return $pages->previousPageKey($this->key());
-	}
-
-	// Returns the next page key
-	public function nextKey()
-	{
-		global $pages;
-		return $pages->nextPageKey($this->key());
-	}
-
-	// Returns the category name
-	public function category()
-	{
-		return $this->categoryMap('name');
-	}
-
-	// Returns the category template
-	public function categoryTemplate()
-	{
-		return $this->categoryMap('template');
-	}
-
-	// Returns the category description
-	public function categoryDescription()
-	{
-		return $this->categoryMap('description');
-	}
-
-	// Returns the category key
-	public function categoryKey()
-	{
-		return $this->getValue('category');
-	}
-
-	// Returns the category permalink
-	public function categoryPermalink()
-	{
-		return DOMAIN_CATEGORIES.$this->categoryKey();
-	}
-
-	// Returns the field from the array
-	// categoryMap = array( 'name'=>'', 'list'=>array() )
-	public function categoryMap($field)
-	{
-		global $categories;
-		$categoryKey = $this->categoryKey();
-		$map = $categories->getMap($categoryKey);
-
-		if ($field=='key') {
-			return $this->categoryKey();
-		} elseif(isset($map[$field])) {
-			return $map[$field];
-		}
-
-		return false;
-	}
-
-	// Returns the user object or passing the method returns the object User method
-	public function user($method=false)
-	{
-		$username = $this->username();
-		try {
-			$user = new User($username);
-			if ($method) {
-				return $user->{$method}();
-			}
-			return $user;
-		} catch (Exception $e) {
-			return false;
-		}
-	}
-
-	/*	Returns the template for the page === Bludit v4
-
-		@return		string/boolean	Returns the template for the page or FALSE if the page haven't a template assigned
-	*/
-	public function template()
-	{
-		$template = $this->getValue('template');
-		if (empty($template)) {
-			return false;
-		}
-		return $template;
-	}
-
-	// Returns the description field
-	public function description()
-	{
-		return $this->getValue('description');
-	}
-
-	// Returns the tags separated by comma
-	// (boolean) $returnsArray, TRUE to get the tags as an array, FALSE to get the tags separated by comma
-	// The tags in array format returns array( tagKey => tagName )
-	public function tags($returnsArray=false)
-	{
-		$tags = $this->getValue('tags');
-		if ($returnsArray) {
-			if (empty($tags)) {
-				return array();
-			}
-			return $tags;
-		}
-
-		if (empty($tags)) {
-			return '';
-		}
-		// Return string with tags separated by comma.
-		return implode(',', $tags);
-	}
-
-
-
-	public function json($returnsArray=false)
-	{
-		$tmp['key'] 		= $this->key();
-		$tmp['title'] 		= $this->title();
-		$tmp['content'] 	= $this->content(); // Markdown parsed
-		$tmp['contentRaw'] 	= $this->contentRaw(true); // No Markdown parsed
-		$tmp['description'] = $this->description();
-		$tmp['type'] 		= $this->type();
-		$tmp['slug'] 		= $this->slug();
-		$tmp['date'] 		= $this->date();
-		$tmp['dateRaw'] 	= $this->dateRaw();
-		$tmp['tags'] 		= $this->tags(false);
-		$tmp['username'] 	= $this->username();
-		$tmp['category'] 	= $this->category();
-		$tmp['uuid'] 		= $this->uuid();
-		$tmp['dateUTC']		= Date::convertToUTC($this->dateRaw(), DB_DATE_FORMAT, DB_DATE_FORMAT);
-		$tmp['permalink'] 	= $this->permalink(true);
-		$tmp['coverImage'] 	= $this->coverImage(true);
-		$tmp['coverImageFilename'] 	= $this->coverImage(false);
-
-		if ($returnsArray) {
-			return $tmp;
-		}
-
-		return json_encode($tmp);
-	}
-
-	// Returns the endpoint of the coverimage, FALSE if the page doesn't have a cover image
-	// (boolean) $absolute, TRUE returns the complete URL, FALSE returns the filename
-	// If the user defined an external cover image the function returns it
-	public function coverImage($absolute=true)
-	{
-		$filename = $this->getValue('coverImage');
-		if (empty($filename)) {
-			return false;
-		}
-
-		// Check is external cover image
-		if (filter_var($filename, FILTER_VALIDATE_URL)) {
-			return $filename;
-		}
-
-		if ($absolute) {
-			if (IMAGE_RESTRICT) {
-				return DOMAIN_UPLOADS_PAGES.$this->uuid().'/'.$filename;
-			}
-			return DOMAIN_UPLOADS.$filename;
-		}
-
-		return $filename;
-	}
-
-	// Returns TRUE if the content has the text splited
-	public function readMore()
-	{
-		$content = $this->contentRaw();
-		return Text::stringContains($content, PAGE_BREAK);
-	}
-
-	public function uuid()
-	{
-		return $this->getValue('uuid');
-	}
-
-	// Returns the field key
-	public function key()
-	{
-		return $this->getValue('key');
-	}
-
-	// (boolean) Returns TRUE if the page is published, FALSE otherwise
-	public function published()
-	{
-		return ($this->getValue('type')==='published');
-	}
-
-	// (boolean) Returns TRUE if the page is scheduled, FALSE otherwise
-	public function scheduled()
-	{
-		return ($this->getValue('type')==='scheduled');
-	}
-
-	// (boolean) Returns TRUE if the page is draft, FALSE otherwise
-	public function draft()
-	{
-		return ($this->getValue('type')=='draft');
-	}
-
-	// (boolean) Returns TRUE if the page is autosave, FALSE otherwise
-	public function autosave()
-	{
-		return ($this->getValue('type')=='autosave');
-	}
-
-	// (boolean) Returns TRUE if the page is sticky, FALSE otherwise
-	public function sticky()
-	{
-		return ($this->getValue('type')=='sticky');
-	}
-
-	// (boolean) Returns TRUE if the page is static, FALSE otherwise
-	public function isStatic()
-	{
-		return ($this->getValue('type')=='static');
-	}
-
-	// (boolean) Returns TRUE if the page is unlisted, FALSE otherwise
-	public function unlisted()
-	{
-		return ($this->getValue('type')=='unlisted');
-	}
-
-	// (string) Returns type of the page
-	public function type()
-	{
-		return $this->getValue('type');
-	}
-
-	// Returns the title field
-	public function title()
-	{
-		return $this->getValue('title');
-	}
-
-	// Returns TRUE if the page has enabled the comments, FALSE otherwise
-	public function allowComments()
-	{
-		return $this->getValue('allowComments');
-	}
-
-	// Returns the page position
-	public function position()
-	{
-		return $this->getValue('position');
-	}
-
-	// Returns the page noindex
-	public function noindex()
-	{
-		return $this->getValue('noindex');
-	}
-
-	// Returns the page nofollow
-	public function nofollow()
-	{
-		return $this->getValue('nofollow');
-	}
-
-	// Returns the page noarchive
-	public function noarchive()
-	{
-		return $this->getValue('noarchive');
-	}
-
-	// Returns the page slug
-	public function slug()
-	{
-		$explode = explode('/', $this->key());
-		return end($explode);
-	}
-
-	// Returns the parent key, if the page doesn't have a parent returns FALSE
-	public function parent()
-	{
-		return $this->parentKey();
-	}
-
-	// Returns the parent key, if the page doesn't have a parent returns FALSE
-	public function parentKey()
-	{
-		$explode = explode('/', $this->key());
-		if (isset($explode[1])) {
-			return $explode[0];
-		}
-		return false;
-	}
-
-	// Returns TRUE if the page is a parent, has or not children
-	public function isParent()
-	{
-		return $this->parentKey()===false;
-	}
-
-	// Returns the parent method output, if the page doesn't have a parent returns FALSE
-	public function parentMethod($method)
-	{
-		$parentKey = $this->parentKey();
-		if ($parentKey) {
-			try {
-				$page = new Page($parentKey);
-				return $page->{$method}();
-			} catch (Exception $e) {
-				// Continoue
-			}
-		}
-
-		return false;
-	}
-
-	// Returns TRUE if the page is a child, FALSE otherwise
-	public function isChild()
-	{
-		return $this->parentKey()!==false;
-	}
-
-	// Returns TRUE if the page has children
-	public function hasChildren()
-	{
-		$childrenKeys = $this->childrenKeys();
-		return !empty($childrenKeys);
-	}
-
-	// Returns an array with all children's keys
-	public function childrenKeys()
-	{
-		global $pages;
-		$key = $this->key();
-		return $pages->getChildren($key);
-	}
-
-	// Returns an array with all children as Page-Object
-	public function children()
-	{
-		global $pages;
-		$list = array();
-		$childrenKeys = $pages->getChildren($this->key());
-		foreach ($childrenKeys as $childKey) {
-			try {
-				$child = new Page($childKey);
-				array_push($list, $child);
-			} catch (Exception $e) {
-				// Continue
-			}
-		}
-
-		return $list;
-	}
-
-	/*	Returns the amount of minutes takes to read the page === Bludit v4
-
-		@return				string			Returns the minutes as string
-	*/
-	public function readingTime() {
-		$words = $this->content(true);
-		$words = strip_tags($words);
-		$words = str_word_count($words);
-		$average = $words / 200;
-		$minutes = round($average);
-
-		if ($minutes>1) {
-			return $minutes;
-		}
-		return '~1';
-	}
-
-	// Returns the value from the field, false if the fields doesn't exists
-	// If you set the $option as TRUE, the function returns an array with all the values of the field
-	public function custom($field, $options=false)
-	{
-		if (isset($this->vars['custom'][$field])) {
-			if ($options) {
-				return $this->vars['custom'][$field];
-			}
-			return $this->vars['custom'][$field]['value'];
-		}
-		return false;
-	}
-
-	/*	Returns an array with all pages key related to the page === Bludit v4
-		The relation is based on the tags.
-
-		@sortByDate			boolean			TRUE if you want to get sort by date the pages, FALSE random order
-		@getFirst			int				Amount of related pages, -1 indicates all related pages
-		@return				array			Returns an array with the page keys related to page
-	*/
-	public function related($sortByDate=false, $getFirst=-1) {
-		global $tags;
-		$pageTags = $this->tags(true);
-		$list = array();
-		// For each tag get the list of related pages
-		foreach ($pageTags as $tagKey=>$tagName) {
-			$pagesRelated = $tags->getList($tagKey, 1, -1);
-			$list = array_merge($list, $pagesRelated);
-		}
-
-		// Remove duplicates
-		$list = array_unique($list);
-
-		// Remove himself from the list
-		if (($key = array_search($this->key(), $list)) !== false) {
-			unset($list[$key]);
-		}
-
-		// Sort by date if requested
-		if ($sortByDate) {
-			$listSortByDate = array();
-			foreach ($list as $pageKey) {
-				$tmpPage = new Page($pageKey);
-				$listSortByDate[$tmpPage->date('U')] = $pageKey;
-			}
-			krsort($listSortByDate);
-			$list = $listSortByDate;
-		}
-
-		if ($getFirst==-1) {
-			return $list;
-		} else {
-			return array_slice($list, 0, $getFirst);
-		}
-	}
-
-	/*	Returns relative time (e.g. "1 minute ago") === Bludit v4
-		Based on http://stackoverflow.com/a/18602474
-
-		@complete			boolean			TRUE full version, FALSE short version
-		@return				string			Relative time, for example: 1 minute ago
-	*/
-	public function relativeTime($complete=false) {
-		$current = new DateTime;
-		$past    = new DateTime($this->getValue('dateRaw'));
-		$elapsed = $current->diff($past);
-
-		$elapsed->w  = floor($elapsed->d / 7);
-		$elapsed->d -= $elapsed->w * 7;
-
-		$string = array(
-			'y' => 'year',
-			'm' => 'month',
-			'w' => 'week',
-			'd' => 'day',
-			'h' => 'hour',
-			'i' => 'minute',
-			's' => 'second',
-		);
-
-		foreach ($string as $key => &$value) {
-			if ($elapsed->$key) {
-				$value = $elapsed->$key . ' ' . $value . ($elapsed->$key > 1 ? 's' : ' ');
-			} else {
-				unset($string[$key]);
-			}
-		}
-
-		if (!$complete) {
-			$string = array_slice($string, 0 , 1);
-		}
-
-		return $string ? implode(', ', $string) . ' ago' : 'Just now';
-	}
+    protected $vars;
+
+    function __construct($key)
+    {
+        global $pages;
+
+        $this->vars['key'] = $key;
+        // If key is FALSE, the page is create with default values, like an empty page
+        // Useful for Page Not Found
+        if ($key===false) {
+            $row = $pages->getDefaultFields();
+        } else {
+            if (Text::isEmpty($key) || !$pages->exists($key)) {
+                $errorMessage = 'Page not found in database by key ['.$key.']';
+                Log::set(__METHOD__.LOG_SEP.$errorMessage);
+                throw new Exception($errorMessage);
+            }
+            // The database doesn't have the page's content.
+            $row = $pages->getPageDB($key);
+        }
+
+        foreach ($row as $field=>$value) {
+            if ($field=='date') {
+                $this->vars['dateRaw'] = $value;
+            } else {
+                $this->vars[$field] = $value;
+            }
+        }
+    }
+
+    /**
+     * Returns the value associated to a field, if the field doesn't exists returns FALSE.
+     *
+     * @param string $field
+     * @return bool|string|int|array
+     */
+    public function getValue(string $field): bool|string|int|array
+    {
+        if (isset($this->vars[$field])) {
+            return $this->vars[$field];
+        }
+        return false;
+    }
+
+    /**
+     * Set the value associated to a field.
+     *
+     * @param string $field
+     * @param string $value
+     * @return boolean
+     */
+    public function setField(string $field, string $value): bool
+    {
+        $this->vars[$field] = $value;
+        return true;
+    }
+
+    /**
+     * Returns the full raw page's content. This content is not markdown parser.
+     *
+     * @param boolean $sanitize         TRUE returns the content sanitized
+     * @return string
+     */
+    public function contentRaw(bool $sanitize=false): string
+    {
+        $key = $this->key();
+        $filePath = PATH_PAGES.$key.DS.FILENAME;
+        $contentRaw = file_get_contents($filePath);
+
+        if ($sanitize) {
+            return Sanitize::html($contentRaw);
+        }
+        return $contentRaw;
+    }
+
+    /**
+     * Returns the full page's content.
+     *
+     * @param boolean $sanitize         TRUE returns the content sanitized
+     * @return string
+     */
+    public function content(bool $sanitize=false): string
+    {
+        // If already set the content, return it
+        $content = $this->getValue('content');
+        if (!empty($content)) {
+            return $content;
+        }
+
+        // Get the raw content
+        $content = $this->contentRaw(false);
+
+        // Parse Markdown
+        if (MARKDOWN_PARSER) {
+            $parsedown = new Parsedown();
+            $content = $parsedown->text($content);
+        }
+
+        // Parse img src relative to absolute (with domain)
+        if (IMAGE_RELATIVE_TO_ABSOLUTE) {
+            $domain = IMAGE_RESTRICT?DOMAIN_UPLOADS_PAGES.$this->key().'/':DOMAIN_UPLOADS;
+            $content = Text::imgRel2Abs($content, $domain);
+        }
+
+        if ($sanitize) {
+            return Sanitize::html($content);
+        }
+        return $content;
+    }
+
+    /**
+     * Returns the first part of the content if the content is splited, otherwise is returned the full content.
+     *
+     * @param boolean $sanitize         TRUE returns the content sanitized
+     * @return string
+     */
+    public function contentBreak(bool $sanitize=false): string
+    {
+        $content = $this->content($sanitize);
+        $explode = explode(PAGE_BREAK, $content);
+        return $explode[0];
+    }
+
+    // Returns the date according to locale settings and the format defined in the system
+    public function date($format=false)
+    {
+        $dateRaw = $this->dateRaw();
+        if ($format===false) {
+            global $site;
+            $format = $site->dateFormat();
+        }
+        return Date::format($dateRaw, DB_DATE_FORMAT, $format);
+    }
+
+    // Returns the date according to locale settings and format as database stored
+    public function dateRaw()
+    {
+        // This field is set in the constructor
+        return $this->getValue('dateRaw');
+    }
+
+    // Returns the date according to locale settings and format settings
+    public function dateModified($format=false)
+    {
+        $dateRaw = $this->getValue('dateModified');
+        if ($format===false) {
+            global $site;
+            $format = $site->dateFormat();
+        }
+        return Date::format($dateRaw, DB_DATE_FORMAT, $format);
+    }
+
+    // Returns the username who created the page
+    public function username()
+    {
+        return $this->getValue('username');
+    }
+
+    // TODO: Check if necessary this function
+    public function getDB()
+    {
+        return $this->vars;
+    }
+
+    // Returns the permalink
+    // (boolean) $absolute, TRUE returns the page link with the DOMAIN, FALSE without the DOMAIN
+    public function permalink($absolute=true)
+    {
+        // Get the key of the page
+        $key = $this->key();
+
+        if($absolute) {
+            return DOMAIN_PAGES.$key;
+        }
+
+        return HTML_PATH_ROOT.PAGE_URI_FILTER.$key;
+    }
+
+    public function url($absolute=true)
+    {
+        return $this->permalink($absolute);
+    }
+
+    // Returns the previous page key
+    public function previousKey()
+    {
+        global $pages;
+        return $pages->previousPageKey($this->key());
+    }
+
+    // Returns the next page key
+    public function nextKey()
+    {
+        global $pages;
+        return $pages->nextPageKey($this->key());
+    }
+
+    // Returns the category name
+    public function category()
+    {
+        return $this->categoryMap('name');
+    }
+
+    // Returns the category template
+    public function categoryTemplate()
+    {
+        return $this->categoryMap('template');
+    }
+
+    // Returns the category description
+    public function categoryDescription()
+    {
+        return $this->categoryMap('description');
+    }
+
+    // Returns the category key
+    public function categoryKey()
+    {
+        return $this->getValue('category');
+    }
+
+    // Returns the category permalink
+    public function categoryPermalink()
+    {
+        return DOMAIN_CATEGORIES.$this->categoryKey();
+    }
+
+    // Returns the field from the array
+    // categoryMap = array( 'name'=>'', 'list'=>array() )
+    public function categoryMap($field)
+    {
+        global $categories;
+        $categoryKey = $this->categoryKey();
+        $map = $categories->getMap($categoryKey);
+
+        if ($field=='key') {
+            return $this->categoryKey();
+        } elseif(isset($map[$field])) {
+            return $map[$field];
+        }
+
+        return false;
+    }
+
+    // Returns the user object or passing the method returns the object User method
+    public function user($method=false)
+    {
+        $username = $this->username();
+        try {
+            $user = new User($username);
+            if ($method) {
+                return $user->{$method}();
+            }
+            return $user;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the template for the page or FALSE if the page haven't a template assigned.
+     *
+     * @return boolean|string
+     */
+    public function template(): bool|string
+    {
+        $template = $this->getValue('template');
+        if (empty($template)) {
+            return false;
+        }
+        return $template;
+    }
+
+    // Returns the description field
+    public function description()
+    {
+        return $this->getValue('description');
+    }
+
+    // Returns the tags separated by comma
+    // (boolean) $returnsArray, TRUE to get the tags as an array, FALSE to get the tags separated by comma
+    // The tags in array format returns array( tagKey => tagName )
+    public function tags($returnsArray=false)
+    {
+        $tags = $this->getValue('tags');
+        if ($returnsArray) {
+            if (empty($tags)) {
+                return array();
+            }
+            return $tags;
+        }
+
+        if (empty($tags)) {
+            return '';
+        }
+        // Return string with tags separated by comma.
+        return implode(',', $tags);
+    }
+
+
+
+    public function json($returnsArray=false)
+    {
+        $tmp['key'] 		= $this->key();
+        $tmp['title'] 		= $this->title();
+        $tmp['content'] 	= $this->content(); // Markdown parsed
+        $tmp['contentRaw'] 	= $this->contentRaw(true); // No Markdown parsed
+        $tmp['description'] = $this->description();
+        $tmp['type'] 		= $this->type();
+        $tmp['slug'] 		= $this->slug();
+        $tmp['date'] 		= $this->date();
+        $tmp['dateRaw'] 	= $this->dateRaw();
+        $tmp['tags'] 		= $this->tags(false);
+        $tmp['username'] 	= $this->username();
+        $tmp['category'] 	= $this->category();
+        $tmp['uuid'] 		= $this->uuid();
+        $tmp['dateUTC']		= Date::convertToUTC($this->dateRaw(), DB_DATE_FORMAT, DB_DATE_FORMAT);
+        $tmp['permalink'] 	= $this->permalink(true);
+        $tmp['coverImage'] 	= $this->coverImage(true);
+        $tmp['coverImageFilename'] 	= $this->coverImage(false);
+
+        if ($returnsArray) {
+            return $tmp;
+        }
+
+        return json_encode($tmp);
+    }
+
+    /**
+     * Returns the cover image endpoint, FALSE if the page doesn't have a cover image
+     *
+     * @param boolean $absolute         TRUE returns the complete URL, FALSE returns the filename
+     * @return string
+     */
+    public function coverImage(bool $absolute=true): string
+    {
+        $filename = $this->getValue('coverImage');
+        if (empty($filename)) {
+            return false;
+        }
+
+        // Check if it is an external cover image
+        if (filter_var($filename, FILTER_VALIDATE_URL)) {
+            return $filename;
+        }
+
+        if ($absolute) {
+            if (IMAGE_RESTRICT) {
+                return DOMAIN_UPLOADS_PAGES.$this->key().'/'.$filename;
+            }
+            return DOMAIN_UPLOADS.$filename;
+        }
+
+        return $filename;
+    }
+
+    // Returns TRUE if the content has the text splited
+    public function readMore()
+    {
+        $content = $this->contentRaw();
+        return Text::stringContains($content, PAGE_BREAK);
+    }
+
+    public function uuid()
+    {
+        return $this->getValue('uuid');
+    }
+
+    // Returns the field key
+    public function key()
+    {
+        return $this->getValue('key');
+    }
+
+    /**
+     * Returns TRUE if the page is type "published", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function published(): bool
+    {
+        return ($this->getValue('type')==='published');
+    }
+
+    /**
+     * Returns TRUE if the page is type "scheduled", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function scheduled(): bool
+    {
+        return ($this->getValue('type')==='scheduled');
+    }
+
+    /**
+     * Returns TRUE if the page is type "draft", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function draft(): bool
+    {
+        return ($this->getValue('type')=='draft');
+    }
+
+    /**
+     * Returns TRUE if the page is type "sticky", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function sticky(): bool
+    {
+        return ($this->getValue('type')=='sticky');
+    }
+
+    /**
+     * Returns TRUE if the page is type "static", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function isStatic(): bool
+    {
+        return ($this->getValue('type')=='static');
+    }
+
+    /**
+     * Returns TRUE if the page is type "unlisted", FALSE otherwise
+     *
+     * @return boolean
+     */
+    public function unlisted(): bool
+    {
+        return ($this->getValue('type')=='unlisted');
+    }
+
+    // (boolean) Returns TRUE if the page is autosave, FALSE otherwise
+    public function autosave()
+    {
+        return ($this->getValue('type')=='autosave');
+    }
+
+    // (string) Returns type of the page
+    public function type()
+    {
+        return $this->getValue('type');
+    }
+
+    // Returns the title field
+    public function title()
+    {
+        return $this->getValue('title');
+    }
+
+    // Returns TRUE if the page has enabled the comments, FALSE otherwise
+    public function allowComments()
+    {
+        return $this->getValue('allowComments');
+    }
+
+    // Returns the page position
+    public function position()
+    {
+        return $this->getValue('position');
+    }
+
+    // Returns the page noindex
+    public function noindex()
+    {
+        return $this->getValue('noindex');
+    }
+
+    // Returns the page nofollow
+    public function nofollow()
+    {
+        return $this->getValue('nofollow');
+    }
+
+    // Returns the page noarchive
+    public function noarchive()
+    {
+        return $this->getValue('noarchive');
+    }
+
+    // Returns the page slug
+    public function slug()
+    {
+        $explode = explode('/', $this->key());
+        return end($explode);
+    }
+
+    // Returns the parent key, if the page doesn't have a parent returns FALSE
+    public function parent()
+    {
+        return $this->parentKey();
+    }
+
+    // Returns the parent key, if the page doesn't have a parent returns FALSE
+    public function parentKey()
+    {
+        $explode = explode('/', $this->key());
+        if (isset($explode[1])) {
+            return $explode[0];
+        }
+        return false;
+    }
+
+    // Returns TRUE if the page is a parent, has or not children
+    public function isParent()
+    {
+        return $this->parentKey()===false;
+    }
+
+    // Returns the parent method output, if the page doesn't have a parent returns FALSE
+    public function parentMethod($method)
+    {
+        $parentKey = $this->parentKey();
+        if ($parentKey) {
+            try {
+                $page = new Page($parentKey);
+                return $page->{$method}();
+            } catch (Exception $e) {
+                // Continoue
+            }
+        }
+
+        return false;
+    }
+
+    // Returns TRUE if the page is a child, FALSE otherwise
+    public function isChild()
+    {
+        return $this->parentKey()!==false;
+    }
+
+    // Returns TRUE if the page has children
+    public function hasChildren()
+    {
+        $childrenKeys = $this->childrenKeys();
+        return !empty($childrenKeys);
+    }
+
+    // Returns an array with all children's keys
+    public function childrenKeys()
+    {
+        global $pages;
+        $key = $this->key();
+        return $pages->getChildren($key);
+    }
+
+    // Returns an array with all children as Page-Object
+    public function children()
+    {
+        global $pages;
+        $list = array();
+        $childrenKeys = $pages->getChildren($this->key());
+        foreach ($childrenKeys as $childKey) {
+            try {
+                $child = new Page($childKey);
+                array_push($list, $child);
+            } catch (Exception $e) {
+                // Continue
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Returns the amount of minutes takes to read the page
+     *
+     * @return string
+     */
+    public function readingTime(): string
+    {
+        $words = $this->content(true);
+        $words = strip_tags($words);
+        $words = str_word_count($words);
+        $average = $words / 200;
+        $minutes = round($average);
+
+        if ($minutes>1) {
+            return $minutes;
+        }
+        return '~1';
+    }
+
+    // Returns the value from the field, false if the fields doesn't exists
+    // If you set the $option as TRUE, the function returns an array with all the values of the field
+    public function custom($field, $options=false)
+    {
+        if (isset($this->vars['custom'][$field])) {
+            if ($options) {
+                return $this->vars['custom'][$field];
+            }
+            return $this->vars['custom'][$field]['value'];
+        }
+        return false;
+    }
+
+    /**
+     * Returns an array with all pages' keys related to the page. The relation is based on the tags.
+     *
+     * @param boolean $sortByDate       TRUE if you want to get sort by date the pages, FALSE random order
+     * @param integer $getFirst         Amount of related pages, -1 indicates all related pages
+     * @return array
+     */
+    public function related(bool $sortByDate=false, int $getFirst=-1): array
+    {
+        global $tags;
+        $pageTags = $this->tags(true);
+        $list = array();
+        // For each tag get the list of related pages
+        foreach ($pageTags as $tagKey=>$tagName) {
+            $pagesRelated = $tags->getList($tagKey, 1, -1);
+            $list = array_merge($list, $pagesRelated);
+        }
+
+        // Remove duplicates
+        $list = array_unique($list);
+
+        // Remove himself from the list
+        if (($key = array_search($this->key(), $list)) !== false) {
+            unset($list[$key]);
+        }
+
+        // Sort by date if requested
+        if ($sortByDate) {
+            $listSortByDate = array();
+            foreach ($list as $pageKey) {
+                $tmpPage = new Page($pageKey);
+                $listSortByDate[$tmpPage->date('U')] = $pageKey;
+            }
+            krsort($listSortByDate);
+            $list = $listSortByDate;
+        }
+
+        if ($getFirst==-1) {
+            return $list;
+        } else {
+            return array_slice($list, 0, $getFirst);
+        }
+    }
+
+    /**
+     * Returns the date as relative time.
+     *
+     * @param boolean $complete     TRUE full version, FALSE short version
+     * @return string               Relative time, for example: 1 minute ago
+     */
+    public function relativeTime(bool $complete=false): string
+    {
+        $current = new DateTime;
+        $past    = new DateTime($this->getValue('dateRaw'));
+        $elapsed = $current->diff($past);
+
+        $elapsed->w  = floor($elapsed->d / 7);
+        $elapsed->d -= $elapsed->w * 7;
+
+        $string = array(
+            'y' => 'year',
+            'm' => 'month',
+            'w' => 'week',
+            'd' => 'day',
+            'h' => 'hour',
+            'i' => 'minute',
+            's' => 'second',
+        );
+
+        foreach ($string as $key => &$value) {
+            if ($elapsed->$key) {
+                $value = $elapsed->$key . ' ' . $value . ($elapsed->$key > 1 ? 's' : ' ');
+            } else {
+                unset($string[$key]);
+            }
+        }
+
+        if (!$complete) {
+            $string = array_slice($string, 0 , 1);
+        }
+
+        return $string ? implode(', ', $string) . ' ago' : 'Just now';
+    }
 
 }
diff --git a/bl-kernel/pages.class.php b/bl-kernel/pages.class.php
index b534781a..9d1159d1 100644
--- a/bl-kernel/pages.class.php
+++ b/bl-kernel/pages.class.php
@@ -34,17 +34,18 @@ class Pages extends dbJSON {
         return $this->dbFields;
     }
 
-    /*	Get the database row associated to a page
-
-        @key			string				The key of the page to be fetch
-        @return		array/boolean		Return an array with the database for a page, FALSE otherwise
-    */
-    public function getPageDB($key)
+    /**
+     * Returns the table row associated to a particular page. The page's content is not included.
+     * For example in SQL, SELECT * FROM pages WHERE key = $key
+     *
+     * @param string $key       The key of the page to be fetch
+     * @return array            Return an array with the database for a page, FALSE otherwise
+     */
+    public function getPageDB(string $key): array
     {
         if ($this->exists($key)) {
             return $this->db[$key];
         }
-
         return false;
     }
 
@@ -57,12 +58,13 @@ class Pages extends dbJSON {
         return isset ($this->db[$key]);
     }
 
-    /*	Create a new page === Bludit v4
-
-        @args			array			The array $args supports all the keys from the variable $dbFields. If you don't pass all the keys, the default values are used.
-        @return		string/boolean	Returns the page key if the page is successfully created, FALSE otherwise
-    */
-    public function add($args)
+    /**
+     * Creates a new page.
+     *
+     * @param array $args       The array $args supports all the keys from the variable $dbFields. If you don't pass all the keys, the default values are used.
+     * @return boolean|string   Returns the page key if the page is successfully created, FALSE otherwise
+     */
+    public function add(array $args): bool|string
     {
         $row = array();
 
@@ -175,12 +177,13 @@ class Pages extends dbJSON {
         return $key;
     }
 
-    /*	Edit a page === Bludit v4
-
-        @args			array			The array $args supports all the keys from the variable $dbFields. If you don't pass all the keys, the default values are used.
-        @return		string/boolean	Returns the page key if the page is successfully edited, FALSE otherwise
-    */
-    public function edit($args)
+    /**
+     * Edit a page.
+     *
+     * @param array $args       The array $args supports all the keys from the variable $dbFields. If you don't pass all the keys, the default values are used.
+     * @return boolean|string   Returns the page key if the page is successfully edited, FALSE otherwise
+     */
+    public function edit(array $args): bool|string
     {
         // This is the new row for the table and is going to replace the old row
         $row = array();
@@ -300,12 +303,13 @@ class Pages extends dbJSON {
         return $newKey;
     }
 
-    /*	Delete a page === Bludit v4
-
-        @key			string			The key of the page to be deleted
-        @return		boolean			Returns TRUE if the page was deleted successfully, FALSE otherwise
-    */
-    public function delete($key)
+    /**
+     * Delete a page.
+     *
+     * @param string $key       The key of the page to be deleted
+     * @return boolean          Returns TRUE if the page was deleted successfully, FALSE otherwise
+     */
+    public function delete(string $key): bool
     {
         // This is need it, because if the key is empty the Filesystem::deleteRecursive is going to delete PATH_PAGES
         if (empty($key)) {
@@ -485,6 +489,26 @@ class Pages extends dbJSON {
         return $tmp;
     }
 
+    /**
+     * Returns an array with all unlisted pages
+     *
+     * @param boolean $onlyKeys         If TRUE returns only the pages' keys
+     * @return array
+     */
+    public function getUnlistedDB(bool $onlyKeys=true): array
+    {
+        $tmp = $this->db;
+        foreach ($tmp as $key=>$fields) {
+            if($fields['type']!='unlisted') {
+                unset($tmp[$key]);
+            }
+        }
+        if ($onlyKeys) {
+            return array_keys($tmp);
+        }
+        return $tmp;
+    }
+
     // Returns the next number of the bigger position
     public function nextPositionNumber()
     {
@@ -530,12 +554,18 @@ class Pages extends dbJSON {
     }
 
     /**
-     * Get a list of pages' keys. === Bludit v4
-     * @param       int         $pageNumber     Page number for the paginator
-     * @param       int         $numberOfItems  Amount of items to return, if -1 returns all the items
-     * @return      array|bool                  Returns an array with the pages' keys or FALSE if it out of range
+     * Get a list of pages' keys.
+     *
+     * @param integer $pageNumber       Page number for the paginator
+     * @param integer $numberOfItems    Amount of items to return, if -1 returns all the items
+     * @param boolean $published
+     * @param boolean $static
+     * @param boolean $sticky
+     * @param boolean $draft
+     * @param boolean $scheduled
+     * @return boolean|array            Returns an array with the pages' keys or FALSE if it out of range
      */
-    public function getList(int $pageNumber, int $numberOfItems, bool $published=true, bool $static=false, bool $sticky=false, bool $draft=false, bool $scheduled=false)
+    public function getList(int $pageNumber, int $numberOfItems, bool $published=true, bool $static=false, bool $sticky=false, bool $draft=false, bool $scheduled=false): bool|array
     {
         $list = array();
         foreach ($this->db as $key=>$fields) {
diff --git a/bl-languages/en.json b/bl-languages/en.json
index 6b7344bc..b334d843 100644
--- a/bl-languages/en.json
+++ b/bl-languages/en.json
@@ -58,10 +58,12 @@
     "thanks-for-supporting-bludit": "Thanks for supporting Bludit",
     "upgrade-to-bludit-pro": "Upgrade to Bludit PRO",
     "language": "Language",
+    "unlisted": "Unlisted",
     "plugin": "Plugin",
     "plugins": "Plugins",
     "developers": "Developers",
     "themes": "Themes",
+    "theme": "Theme",
     "about": "About",
     "url": "URL",
     "welcome": "Welcome",