`
- * @param PageInterface $page The current page object
- * @return string Returns final HTML string
+ * @param string $html HTML tag e.g. `
`
+ * @param PageInterface|null $page Page, defaults to the current page object
+ * @return string Returns final HTML string
*/
- public static function processImageHtml($html, PageInterface $page)
+ public static function processImageHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
@@ -112,157 +108,29 @@ class Excerpts
* Process a Link excerpt
*
* @param array $excerpt
- * @param PageInterface $page
+ * @param PageInterface|null $page Page, defaults to the current page object
* @param string $type
* @return mixed
*/
- public static function processLinkExcerpt($excerpt, PageInterface $page, $type = 'link')
+ public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link')
{
- $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
+ $excerpts = new ExcerptsObject($page);
- $url_parts = static::parseUrl($url);
-
- // If there is a query, then parse it and build action calls.
- if (isset($url_parts['query'])) {
- $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
- $parts = explode('=', $item, 2);
- $value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
- $carry[$parts[0]] = $value;
-
- return $carry;
- }, []);
-
- // Valid attributes supported.
- $valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
-
- // Unless told to not process, go through actions.
- if (array_key_exists('noprocess', $actions)) {
- unset($actions['noprocess']);
- } else {
- // Loop through actions for the image and call them.
- foreach ($actions as $attrib => $value) {
- $key = $attrib;
-
- if (in_array($attrib, $valid_attributes, true)) {
- // support both class and classes.
- if ($attrib === 'classes') {
- $attrib = 'class';
- }
- $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
- unset($actions[$key]);
- }
- }
- }
-
- $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
- }
-
- // If no query elements left, unset query.
- if (empty($url_parts['query'])) {
- unset ($url_parts['query']);
- }
-
- // Set path to / if not set.
- if (empty($url_parts['path'])) {
- $url_parts['path'] = '';
- }
-
- // If scheme isn't http(s)..
- if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
- // Handle custom streams.
- if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
- $url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
- unset($url_parts['stream'], $url_parts['scheme']);
- }
-
- $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
- return $excerpt;
- }
-
- // Handle paths and such.
- $url_parts = Uri::convertUrl($page, $url_parts, $type);
-
- // Build the URL from the component parts and set it on the element.
- $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
-
- return $excerpt;
+ return $excerpts->processLinkExcerpt($excerpt, $type);
}
/**
* Process an image excerpt
*
* @param array $excerpt
- * @param PageInterface $page
+ * @param PageInterface|null $page Page, defaults to the current page object
* @return array
*/
- public static function processImageExcerpt(array $excerpt, PageInterface $page)
+ public static function processImageExcerpt(array $excerpt, PageInterface $page = null)
{
- $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
- $url_parts = static::parseUrl($url);
+ $excerpts = new ExcerptsObject($page);
- $media = null;
- $filename = null;
-
- if (!empty($url_parts['stream'])) {
- $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
-
- $media = $page->getMedia();
-
- } else {
- $grav = Grav::instance();
-
- // File is also local if scheme is http(s) and host matches.
- $local_file = isset($url_parts['path'])
- && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
- && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
-
- if ($local_file) {
- $filename = basename($url_parts['path']);
- $folder = dirname($url_parts['path']);
-
- // Get the local path to page media if possible.
- if ($folder === $page->url(false, false, false)) {
- // Get the media objects for this page.
- $media = $page->getMedia();
- } else {
- // see if this is an external page to this one
- $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
- $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
-
- /** @var PageInterface $ext_page */
- $ext_page = $grav['pages']->dispatch($page_route, true);
- if ($ext_page) {
- $media = $ext_page->getMedia();
- } else {
- $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
- }
- }
- }
- }
-
- // If there is a media file that matches the path referenced..
- if ($media && $filename && isset($media[$filename])) {
- // Get the medium object.
- /** @var Medium $medium */
- $medium = $media[$filename];
-
- // Process operations
- $medium = static::processMediaActions($medium, $url_parts);
- $element_excerpt = $excerpt['element']['attributes'];
-
- $alt = $element_excerpt['alt'] ?? '';
- $title = $element_excerpt['title'] ?? '';
- $class = $element_excerpt['class'] ?? '';
- $id = $element_excerpt['id'] ?? '';
-
- $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
-
- } else {
- // Not a current page media file, see if it needs converting to relative.
- $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
- }
-
- return $excerpt;
+ return $excerpts->processImageExcerpt($excerpt);
}
/**
@@ -270,104 +138,13 @@ class Excerpts
*
* @param Medium $medium
* @param string|array $url
+ * @param PageInterface|null $page Page, defaults to the current page object
* @return Medium
*/
- public static function processMediaActions($medium, $url)
+ public static function processMediaActions($medium, $url, PageInterface $page = null)
{
- if (!is_array($url)) {
- $url_parts = parse_url($url);
- } else {
- $url_parts = $url;
- }
+ $excerpts = new ExcerptsObject($page);
- $actions = [];
-
- // if there is a query, then parse it and build action calls
- if (isset($url_parts['query'])) {
- $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
- $parts = explode('=', $item, 2);
- $value = $parts[1] ?? null;
- $carry[] = ['method' => $parts[0], 'params' => $value];
-
- return $carry;
- }, []);
- }
-
- if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
- $actions[] = ['method' => 'fixOrientation', 'params' => ''];
- }
- $defaults = Grav::instance()['config']->get('system.images.defaults');
- if (is_array($defaults) && count($defaults)) {
- foreach ($defaults as $method => $params) {
- $actions[] = [
- 'method' => $method,
- 'params' => $params,
- ];
- }
- }
-
- // loop through actions for the image and call them
- foreach ($actions as $action) {
- $matches = [];
-
- if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
- $args = [explode(',', $matches[1])];
- } else {
- $args = explode(',', $action['params']);
- }
-
- $medium = call_user_func_array([$medium, $action['method']], $args);
- }
-
- if (isset($url_parts['fragment'])) {
- $medium->urlHash($url_parts['fragment']);
- }
-
- return $medium;
- }
-
- /**
- * Variation of parse_url() which works also with local streams.
- *
- * @param string $url
- * @return array|bool
- */
- protected static function parseUrl($url)
- {
- $url_parts = Utils::multibyteParseUrl($url);
-
- if (isset($url_parts['scheme'])) {
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
-
- // Special handling for the streams.
- if ($locator->schemeExists($url_parts['scheme'])) {
- if (isset($url_parts['host'])) {
- // Merge host and path into a path.
- $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
- unset($url_parts['host']);
- }
-
- $url_parts['stream'] = true;
- }
- }
-
- return $url_parts;
- }
-
- /**
- * @param string $url
- * @return bool|string
- */
- protected static function resolveStream($url)
- {
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
-
- if ($locator->isStream($url)) {
- return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
- }
-
- return $url;
+ return $excerpts->processMediaActions($medium, $url);
}
}
diff --git a/system/src/Grav/Common/Helpers/Truncator.php b/system/src/Grav/Common/Helpers/Truncator.php
index 4c7d7f9..d116bb3 100644
--- a/system/src/Grav/Common/Helpers/Truncator.php
+++ b/system/src/Grav/Common/Helpers/Truncator.php
@@ -234,7 +234,7 @@ class Truncator {
}
/**
- * @inheritDoc
+ *
*/
public function truncate(
$text,
diff --git a/system/src/Grav/Common/Helpers/YamlLinter.php b/system/src/Grav/Common/Helpers/YamlLinter.php
index 43bde82..b7e608d 100644
--- a/system/src/Grav/Common/Helpers/YamlLinter.php
+++ b/system/src/Grav/Common/Helpers/YamlLinter.php
@@ -20,7 +20,8 @@ class YamlLinter
{
$errors = static::lintConfig();
$errors = $errors + static::lintPages();
-
+ $errors = $errors + static::lintBlueprints();
+
return $errors;
}
@@ -34,6 +35,18 @@ class YamlLinter
return static::recurseFolder('config://');
}
+ public static function lintBlueprints()
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+
+ $current_theme = Grav::instance()['config']->get('system.pages.theme');
+ $theme_path = 'themes://' . $current_theme . '/blueprints';
+
+ $locator->addPath('blueprints', '', [$theme_path]);
+ return static::recurseFolder('blueprints://');
+ }
+
public static function recurseFolder($path, $extensions = 'md|yaml')
{
$lint_errors = [];
diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php
index cbc1b5d..a625146 100644
--- a/system/src/Grav/Common/Language/Language.php
+++ b/system/src/Grav/Common/Language/Language.php
@@ -104,6 +104,11 @@ class Language
public function getAvailable()
{
$languagesArray = $this->languages; //Make local copy
+
+ $languagesArray = array_map(function($value) {
+ return preg_quote($value);
+ }, $languagesArray);
+
sort($languagesArray);
return implode('|', array_reverse($languagesArray));
@@ -229,7 +234,7 @@ class Language
}
/**
- * Get's a URL prefix based on configuration
+ * Get a URL prefix based on configuration
*
* @param string|null $lang
* @return string
diff --git a/system/src/Grav/Common/Markdown/Parsedown.php b/system/src/Grav/Common/Markdown/Parsedown.php
index d2f72fc..32033dd 100644
--- a/system/src/Grav/Common/Markdown/Parsedown.php
+++ b/system/src/Grav/Common/Markdown/Parsedown.php
@@ -10,6 +10,7 @@
namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Markdown\Excerpts;
class Parsedown extends \Parsedown
{
@@ -18,12 +19,21 @@ class Parsedown extends \Parsedown
/**
* Parsedown constructor.
*
- * @param PageInterface $page
+ * @param Excerpts|null $excerpts
* @param array|null $defaults
*/
- public function __construct($page, $defaults)
+ public function __construct($excerpts = null, $defaults = null)
{
- $this->init($page, $defaults);
+ if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
+ // Deprecated in Grav 1.6.10
+ if ($defaults) {
+ $defaults = ['markdown' => $defaults];
+ }
+ $excerpts = new Excerpts($excerpts, $defaults);
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
+ }
+
+ $this->init($excerpts, $defaults);
}
}
diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php
index ac04771..a562d8d 100644
--- a/system/src/Grav/Common/Markdown/ParsedownExtra.php
+++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php
@@ -10,6 +10,7 @@
namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Markdown\Excerpts;
class ParsedownExtra extends \ParsedownExtra
{
@@ -18,14 +19,23 @@ class ParsedownExtra extends \ParsedownExtra
/**
* ParsedownExtra constructor.
*
- * @param PageInterface $page
+ * @param Excerpts|null $excerpts
* @param array|null $defaults
* @throws \Exception
*/
- public function __construct($page, $defaults)
+ public function __construct($excerpts = null, $defaults = null)
{
+ if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
+ // Deprecated in Grav 1.6.10
+ if ($defaults) {
+ $defaults = ['markdown' => $defaults];
+ }
+ $excerpts = new Excerpts($excerpts, $defaults);
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
+ }
+
parent::__construct();
- $this->init($page, $defaults);
+ $this->init($excerpts, $defaults);
}
}
diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
index fa8fa91..aa76cdf 100644
--- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
+++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
@@ -9,15 +9,13 @@
namespace Grav\Common\Markdown;
-use Grav\Common\Grav;
-use Grav\Common\Helpers\Excerpts;
+use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Page\Interfaces\PageInterface;
-use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait
{
- /** @var PageInterface $page */
- protected $page;
+ /** @var Excerpts */
+ protected $excerpts;
protected $special_chars;
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
@@ -28,28 +26,49 @@ trait ParsedownGravTrait
/**
* Initialization function to setup key variables needed by the MarkdownGravLinkTrait
*
- * @param PageInterface $page
+ * @param PageInterface|Excerpts|null $excerpts
* @param array|null $defaults
*/
- protected function init($page, $defaults)
+ protected function init($excerpts = null, $defaults = null)
{
- $grav = Grav::instance();
-
- $this->page = $page;
- $this->BlockTypes['{'] [] = 'TwigTag';
- $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
-
- if ($defaults === null) {
- $defaults = (array)Grav::instance()['config']->get('system.pages.markdown');
+ if (!$excerpts || $excerpts instanceof PageInterface) {
+ // Deprecated in Grav 1.6.10
+ if ($defaults) {
+ $defaults = ['markdown' => $defaults];
+ }
+ $this->excerpts = new Excerpts($excerpts, $defaults);
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
+ } else {
+ $this->excerpts = $excerpts;
}
- $this->setBreaksEnabled($defaults['auto_line_breaks']);
- $this->setUrlsLinked($defaults['auto_url_links']);
- $this->setMarkupEscaped($defaults['escape_markup']);
- $this->setSpecialChars($defaults['special_chars']);
+ $this->BlockTypes['{'][] = 'TwigTag';
+ $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
- $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this, 'page' => $page]));
+ $defaults = $this->excerpts->getConfig();
+ if (isset($defaults['markdown']['auto_line_breaks'])) {
+ $this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']);
+ }
+ if (isset($defaults['markdown']['auto_url_links'])) {
+ $this->setUrlsLinked($defaults['markdown']['auto_url_links']);
+ }
+ if (isset($defaults['markdown']['escape_markup'])) {
+ $this->setMarkupEscaped($defaults['markdown']['escape_markup']);
+ }
+ if (isset($defaults['markdown']['special_chars'])) {
+ $this->setSpecialChars($defaults['markdown']['special_chars']);
+ }
+
+ $this->excerpts->fireInitializedEvent($this);
+ }
+
+ /**
+ * @return Excerpts
+ */
+ public function getExcerpts()
+ {
+ return $this->excerpts;
}
/**
@@ -114,7 +133,8 @@ trait ParsedownGravTrait
*/
protected function isBlockContinuable($Type)
{
- $continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue');
+ $continuable = \in_array($Type, $this->continuable_blocks, true)
+ || method_exists($this, 'block' . $Type . 'Continue');
return $continuable;
}
@@ -128,7 +148,8 @@ trait ParsedownGravTrait
*/
protected function isBlockCompletable($Type)
{
- $completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete');
+ $completable = \in_array($Type, $this->completable_blocks, true)
+ || method_exists($this, 'block' . $Type . 'Complete');
return $completable;
}
@@ -210,7 +231,7 @@ trait ParsedownGravTrait
// if this is an image process it
if (isset($excerpt['element']['attributes']['src'])) {
- $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page);
+ $excerpt = $this->excerpts->processImageExcerpt($excerpt);
}
return $excerpt;
@@ -218,11 +239,7 @@ trait ParsedownGravTrait
protected function inlineLink($excerpt)
{
- if (isset($excerpt['type'])) {
- $type = $excerpt['type'];
- } else {
- $type = 'link';
- }
+ $type = $excerpt['type'] ?? 'link';
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
@@ -238,13 +255,15 @@ trait ParsedownGravTrait
// if this is a link
if (isset($excerpt['element']['attributes']['href'])) {
- $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type);
+ $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type);
}
return $excerpt;
}
- // For extending this class via plugins
+ /**
+ * For extending this class via plugins
+ */
public function __call($method, $args)
{
if (isset($this->{$method}) === true) {
diff --git a/system/src/Grav/Common/Page/Markdown/Excerpts.php b/system/src/Grav/Common/Page/Markdown/Excerpts.php
new file mode 100644
index 0000000..eaa13cb
--- /dev/null
+++ b/system/src/Grav/Common/Page/Markdown/Excerpts.php
@@ -0,0 +1,329 @@
+page = $page ?? Grav::instance()['page'] ?? null;
+
+ // Add defaults to the configuration.
+ if (null === $config || !isset($config['markdown'], $config['images'])) {
+ $c = Grav::instance()['config'];
+ $config = $config ?? [];
+ $config += [
+ 'markdown' => $c->get('system.pages.markdown', []),
+ 'images' => $c->get('system.images', [])
+ ];
+ }
+
+ $this->config = $config;
+ }
+
+ public function getPage(): PageInterface
+ {
+ return $this->page;
+ }
+
+ public function getConfig(): array
+ {
+ return $this->config;
+ }
+
+ public function fireInitializedEvent($markdown): void
+ {
+ $grav = Grav::instance();
+
+ $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page]));
+ }
+
+ /**
+ * Process a Link excerpt
+ *
+ * @param array $excerpt
+ * @param string $type
+ * @return array
+ */
+ public function processLinkExcerpt(array $excerpt, string $type = 'link'): array
+ {
+ $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
+
+ $url_parts = $this->parseUrl($url);
+
+ // If there is a query, then parse it and build action calls.
+ if (isset($url_parts['query'])) {
+ $actions = array_reduce(
+ explode('&', $url_parts['query']),
+ static function ($carry, $item) {
+ $parts = explode('=', $item, 2);
+ $value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
+ $carry[$parts[0]] = $value;
+
+ return $carry;
+ },
+ []
+ );
+
+ // Valid attributes supported.
+ $valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
+
+ // Unless told to not process, go through actions.
+ if (array_key_exists('noprocess', $actions)) {
+ unset($actions['noprocess']);
+ } else {
+ // Loop through actions for the image and call them.
+ foreach ($actions as $attrib => $value) {
+ $key = $attrib;
+
+ if (in_array($attrib, $valid_attributes, true)) {
+ // support both class and classes.
+ if ($attrib === 'classes') {
+ $attrib = 'class';
+ }
+ $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
+ unset($actions[$key]);
+ }
+ }
+ }
+
+ $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
+ }
+
+ // If no query elements left, unset query.
+ if (empty($url_parts['query'])) {
+ unset ($url_parts['query']);
+ }
+
+ // Set path to / if not set.
+ if (empty($url_parts['path'])) {
+ $url_parts['path'] = '';
+ }
+
+ // If scheme isn't http(s)..
+ if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
+ // Handle custom streams.
+ if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
+ $grav = Grav::instance();
+ $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
+ unset($url_parts['stream'], $url_parts['scheme']);
+ }
+
+ $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
+
+ return $excerpt;
+ }
+
+ // Handle paths and such.
+ $url_parts = Uri::convertUrl($this->page, $url_parts, $type);
+
+ // Build the URL from the component parts and set it on the element.
+ $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
+
+ return $excerpt;
+ }
+
+ /**
+ * Process an image excerpt
+ *
+ * @param array $excerpt
+ * @return array
+ */
+ public function processImageExcerpt(array $excerpt): array
+ {
+ $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
+ $url_parts = $this->parseUrl($url);
+
+ $media = null;
+ $filename = null;
+
+ if (!empty($url_parts['stream'])) {
+ $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
+
+ $media = $this->page->getMedia();
+
+ } else {
+ $grav = Grav::instance();
+
+ // File is also local if scheme is http(s) and host matches.
+ $local_file = isset($url_parts['path'])
+ && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
+ && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
+
+ if ($local_file) {
+ $filename = basename($url_parts['path']);
+ $folder = dirname($url_parts['path']);
+
+ // Get the local path to page media if possible.
+ if ($this->page && $folder === $this->page->url(false, false, false)) {
+ // Get the media objects for this page.
+ $media = $this->page->getMedia();
+ } else {
+ // see if this is an external page to this one
+ $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
+ $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
+
+ /** @var PageInterface $ext_page */
+ $ext_page = $grav['pages']->dispatch($page_route, true);
+ if ($ext_page) {
+ $media = $ext_page->getMedia();
+ } else {
+ $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
+ }
+ }
+ }
+ }
+
+ // If there is a media file that matches the path referenced..
+ if ($media && $filename && isset($media[$filename])) {
+ // Get the medium object.
+ /** @var Medium $medium */
+ $medium = $media[$filename];
+
+ // Process operations
+ $medium = $this->processMediaActions($medium, $url_parts);
+ $element_excerpt = $excerpt['element']['attributes'];
+
+ $alt = $element_excerpt['alt'] ?? '';
+ $title = $element_excerpt['title'] ?? '';
+ $class = $element_excerpt['class'] ?? '';
+ $id = $element_excerpt['id'] ?? '';
+
+ $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
+
+ } else {
+ // Not a current page media file, see if it needs converting to relative.
+ $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
+ }
+
+ return $excerpt;
+ }
+
+ /**
+ * Process media actions
+ *
+ * @param Medium $medium
+ * @param string|array $url
+ * @return Medium|Link
+ */
+ public function processMediaActions($medium, $url)
+ {
+ $url_parts = is_string($url) ? $this->parseUrl($url) : $url;
+ $actions = [];
+
+ // if there is a query, then parse it and build action calls
+ if (isset($url_parts['query'])) {
+ $actions = array_reduce(
+ explode('&', $url_parts['query']),
+ static function ($carry, $item) {
+ $parts = explode('=', $item, 2);
+ $value = $parts[1] ?? null;
+ $carry[] = ['method' => $parts[0], 'params' => $value];
+
+ return $carry;
+ },
+ []
+ );
+ }
+
+ $config = $this->getConfig();
+ if (!empty($config['images']['auto_fix_orientation'])) {
+ $actions[] = ['method' => 'fixOrientation', 'params' => ''];
+ }
+
+ $defaults = $config['images']['defaults'] ?? [];
+ if (count($defaults)) {
+ foreach ($defaults as $method => $params) {
+ $actions[] = [
+ 'method' => $method,
+ 'params' => $params,
+ ];
+ }
+ }
+
+ // loop through actions for the image and call them
+ foreach ($actions as $action) {
+ $matches = [];
+
+ if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
+ $args = [explode(',', $matches[1])];
+ } else {
+ $args = explode(',', $action['params']);
+ }
+
+ $medium = call_user_func_array([$medium, $action['method']], $args);
+ }
+
+ if (isset($url_parts['fragment'])) {
+ $medium->urlHash($url_parts['fragment']);
+ }
+
+ return $medium;
+ }
+
+ /**
+ * Variation of parse_url() which works also with local streams.
+ *
+ * @param string $url
+ * @return array|bool
+ */
+ protected function parseUrl(string $url)
+ {
+ $url_parts = Utils::multibyteParseUrl($url);
+
+ if (isset($url_parts['scheme'])) {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+
+ // Special handling for the streams.
+ if ($locator->schemeExists($url_parts['scheme'])) {
+ if (isset($url_parts['host'])) {
+ // Merge host and path into a path.
+ $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
+ unset($url_parts['host']);
+ }
+
+ $url_parts['stream'] = true;
+ }
+ }
+
+ return $url_parts;
+ }
+
+ /**
+ * @param string $url
+ * @return bool|string
+ */
+ protected function resolveStream(string $url)
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+
+ if ($locator->isStream($url)) {
+ return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
+ }
+
+ return $url;
+ }
+}
diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
index 0ad4573..f67cd65 100644
--- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php
+++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
@@ -154,6 +154,9 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
*/
public function add($name, $file)
{
+ if (!$file) {
+ return;
+ }
$this->offsetSet($name, $file);
switch ($file->type) {
case 'image':
diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php
index ac29219..1925bef 100644
--- a/system/src/Grav/Common/Page/Medium/ImageMedium.php
+++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php
@@ -170,8 +170,7 @@ class ImageMedium extends Medium
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $image_path = $locator->findResource('cache://images', true);
- $image_dir = $locator->findResource('cache://images', false);
+ $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
$saved_image_path = $this->saveImage();
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
@@ -181,6 +180,7 @@ class ImageMedium extends Medium
}
if (Utils::startsWith($output, $image_path)) {
+ $image_dir = $locator->findResource('cache://images', false);
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
}
@@ -232,7 +232,7 @@ class ImageMedium extends Medium
}
/**
- * Allows the ability to override the Inmage's Pretty name stored in cache
+ * Allows the ability to override the image's pretty name stored in cache
*
* @param string $name
*/
diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
index 627c361..c6d75bc 100644
--- a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
+++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
@@ -10,6 +10,7 @@
namespace Grav\Common\Page\Medium;
use Grav\Common\Markdown\Parsedown;
+use Grav\Common\Page\Markdown\Excerpts;
trait ParsedownHtmlTrait
{
@@ -33,7 +34,7 @@ trait ParsedownHtmlTrait
$element = $this->parsedownElement($title, $alt, $class, $id, $reset);
if (!$this->parsedown) {
- $this->parsedown = new Parsedown(null, null);
+ $this->parsedown = new Parsedown(new Excerpts());
}
return $this->parsedown->elementToHtml($element);
diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php
index 82ff71c..1730359 100644
--- a/system/src/Grav/Common/Page/Page.php
+++ b/system/src/Grav/Common/Page/Page.php
@@ -19,6 +19,7 @@ use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait;
+use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Taxonomy;
use Grav\Common\Uri;
use Grav\Common\Utils;
@@ -27,7 +28,6 @@ use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
-use Symfony\Component\Yaml\Exception\ParseException;
define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
@@ -819,23 +819,31 @@ class Page implements PageInterface
/** @var Config $config */
$config = Grav::instance()['config'];
- $defaults = (array)$config->get('system.pages.markdown');
+ $markdownDefaults = (array)$config->get('system.pages.markdown');
if (isset($this->header()->markdown)) {
- $defaults = array_merge($defaults, $this->header()->markdown);
+ $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown);
}
// pages.markdown_extra is deprecated, but still check it...
- if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
+ if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
- $defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
+ $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
}
+ $extra = $markdownDefaults['extra'] ?? false;
+ $defaults = [
+ 'markdown' => $markdownDefaults,
+ 'images' => $config->get('system.images', [])
+ ];
+
+ $excerpts = new Excerpts($this, $defaults);
+
// Initialize the preferred variant of Parsedown
- if ($defaults['extra']) {
- $parsedown = new ParsedownExtra($this, $defaults);
+ if ($extra) {
+ $parsedown = new ParsedownExtra($excerpts);
} else {
- $parsedown = new Parsedown($this, $defaults);
+ $parsedown = new Parsedown($excerpts);
}
$this->content = $parsedown->text($this->content);
@@ -1397,12 +1405,12 @@ class Page implements PageInterface
return $this->template_format;
}
- // Use content negotitation via the `accept:` header
- $http_accept = $_SERVER['HTTP_ACCEPT'] ?? false;
+ // Use content negotiation via the `accept:` header
+ $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
if (is_string($http_accept)) {
$negotiator = new Negotiator();
- $supported_types = Grav::instance()['config']->get('system.pages.types', ['html', 'json']);
+ $supported_types = Utils::getSupportPageTypes(['html', 'json']);
$priorities = Utils::getMimeTypes($supported_types);
$media_type = $negotiator->getBest($http_accept, $priorities);
@@ -2847,9 +2855,9 @@ class Page implements PageInterface
$result = [];
foreach ((array)$value as $key => $val) {
if (is_int($key)) {
- $result = $result + $this->evaluate($val)->toArray();
+ $result = $result + $this->evaluate($val, $only_published)->toArray();
} else {
- $result = $result + $this->evaluate([$key => $val])->toArray();
+ $result = $result + $this->evaluate([$key => $val], $only_published)->toArray();
}
}
@@ -2930,7 +2938,7 @@ class Page implements PageInterface
case 'page':
case 'self':
$results = new Collection();
- $results = $results->addPage($page)->nonModular();
+ $results = $results->addPage($page);
break;
case 'descendants':
diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php
index d464278..cd0dcc0 100644
--- a/system/src/Grav/Common/Page/Pages.php
+++ b/system/src/Grav/Common/Page/Pages.php
@@ -88,6 +88,13 @@ class Pages
*/
protected $ignore_hidden;
+ /** @var string */
+ protected $check_method;
+
+ protected $pages_cache_id;
+
+ protected $initialized = false;
+
/**
* @var Types
*/
@@ -98,8 +105,6 @@ class Pages
*/
static protected $home_route;
- protected $pages_cache_id;
-
/**
* Constructor
*
@@ -226,11 +231,20 @@ class Pages
return $this->baseUrl($lang, $absolute) . Uri::filterPath($route);
}
+ public function setCheckMethod($method)
+ {
+ $this->check_method = strtolower($method);
+ }
+
/**
* Class initialization. Must be called before using this class.
*/
public function init()
{
+ if ($this->initialized) {
+ return;
+ }
+
$config = $this->grav['config'];
$this->ignore_files = $config->get('system.pages.ignore_files');
$this->ignore_folders = $config->get('system.pages.ignore_folders');
@@ -240,6 +254,10 @@ class Pages
$this->children = [];
$this->routes = [];
+ if (!$this->check_method) {
+ $this->setCheckMethod($config->get('system.cache.check.method', 'file'));
+ }
+
$this->buildPages();
}
@@ -947,7 +965,7 @@ class Pages
$taxonomy = $this->grav['taxonomy'];
// how should we check for last modified? Default is by file
- switch (strtolower($config->get('system.cache.check.method', 'file'))) {
+ switch ($this->check_method) {
case 'none':
case 'off':
$hash = 0;
diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php
index d85fcb4..8729a51 100644
--- a/system/src/Grav/Common/Plugin.php
+++ b/system/src/Grav/Common/Plugin.php
@@ -151,15 +151,32 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
if (\is_string($params)) {
$dispatcher->addListener($eventName, [$this, $params]);
} elseif (\is_string($params[0])) {
- $dispatcher->addListener($eventName, [$this, $params[0]], $params[1] ?? 0);
+ $dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName));
} else {
foreach ($params as $listener) {
- $dispatcher->addListener($eventName, [$this, $listener[0]], $listener[1] ?? 0);
+ $dispatcher->addListener($eventName, [$this, $listener[0]], $this->getPriority($listener, $eventName));
}
}
}
}
+ /**
+ * @param array $params
+ * @param string $eventName
+ */
+ private function getPriority($params, $eventName)
+ {
+ $grav = Grav::instance();
+ $override = implode('.', ["priorities", $this->name, $eventName, $params[0]]);
+ if ($grav['config']->get($override) !== null)
+ {
+ return $grav['config']->get($override);
+ } elseif (isset($params[1])) {
+ return $params[1];
+ }
+ return 0;
+ }
+
/**
* @param array $events
*/
diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php
index d83c152..358727f 100644
--- a/system/src/Grav/Common/Plugins.php
+++ b/system/src/Grav/Common/Plugins.php
@@ -133,12 +133,25 @@ class Plugins extends Iterator
*/
public static function all()
{
- $plugins = Grav::instance()['plugins'];
+ $grav = Grav::instance();
+ $plugins = $grav['plugins'];
$list = [];
foreach ($plugins as $instance) {
$name = $instance->name;
- $result = self::get($name);
+
+ try {
+ $result = self::get($name);
+ } catch (\Exception $e) {
+ $exception = new \RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e);
+
+ /** @var Debugger $debugger */
+ $debugger = $grav['debugger'];
+ $debugger->addMessage("Plugin {$name} cannot be loaded, please check Exceptions tab", 'error');
+ $debugger->addException($exception);
+
+ continue;
+ }
if ($result) {
$list[$name] = $result;
@@ -185,24 +198,31 @@ class Plugins extends Iterator
$grav = Grav::instance();
$locator = $grav['locator'];
- $filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
- if (!is_file($filePath)) {
+ $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
+
+ if (is_file($file)) {
+ // Local variables available in the file: $grav, $config, $name, $file
+ $class = include_once $file;
+
+ $pluginClassFormat = [
+ 'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
+ 'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin'
+ ];
+
+ foreach ($pluginClassFormat as $pluginClass) {
+ if (class_exists($pluginClass)) {
+ $class = new $pluginClass($name, $grav);
+ break;
+ }
+ }
+ } else {
$grav['log']->addWarning(
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name)
);
return null;
}
- require_once $filePath;
-
- $pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin';
- if (!class_exists($pluginClassName)) {
- $pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin';
- if (!class_exists($pluginClassName)) {
- throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name));
- }
- }
- return new $pluginClassName($name, $grav);
+ return $class;
}
}
diff --git a/system/src/Grav/Common/Processors/RequestProcessor.php b/system/src/Grav/Common/Processors/RequestProcessor.php
index 8cb876a..97564e6 100644
--- a/system/src/Grav/Common/Processors/RequestProcessor.php
+++ b/system/src/Grav/Common/Processors/RequestProcessor.php
@@ -30,10 +30,13 @@ class RequestProcessor extends ProcessorBase
$request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true));
}
+ $uri = $request->getUri();
+ $ext = mb_strtolower(pathinfo($uri->getPath(), PATHINFO_EXTENSION));
+
$request = $request
->withAttribute('grav', $this->container)
->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME)
- ->withAttribute('route', Uri::getCurrentRoute())
+ ->withAttribute('route', Uri::getCurrentRoute()->withExtension($ext))
->withAttribute('referrer', $this->container['uri']->referrer());
$event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]);
diff --git a/system/src/Grav/Common/Service/AccountsServiceProvider.php b/system/src/Grav/Common/Service/AccountsServiceProvider.php
index ca15f5a..e0778be 100644
--- a/system/src/Grav/Common/Service/AccountsServiceProvider.php
+++ b/system/src/Grav/Common/Service/AccountsServiceProvider.php
@@ -27,9 +27,10 @@ class AccountsServiceProvider implements ServiceProviderInterface
public function register(Container $container)
{
$container['accounts'] = function (Container $container) {
- /** @var Debugger $debugger */
- $debugger = $container['debugger'];
- if ($container['config']->get('system.accounts.type') === 'flex') {
+ $type = strtolower(defined('GRAV_USER_INSTANCE') ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'data'));
+ if ($type === 'flex') {
+ /** @var Debugger $debugger */
+ $debugger = $container['debugger'];
$debugger->addMessage('User Accounts: Flex Directory');
return $this->flexAccounts($container);
}
@@ -46,7 +47,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
protected function dataAccounts(Container $container)
{
- define('GRAV_USER_INSTANCE', 'DATA');
+ if (!defined('GRAV_USER_INSTANCE')) {
+ define('GRAV_USER_INSTANCE', 'DATA');
+ }
// Use User class for backwards compatibility.
return new DataUser\UserCollection(User::class);
@@ -54,7 +57,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
protected function flexAccounts(Container $container)
{
- define('GRAV_USER_INSTANCE', 'FLEX');
+ if (!defined('GRAV_USER_INSTANCE')) {
+ define('GRAV_USER_INSTANCE', 'FLEX');
+ }
/** @var Config $config */
$config = $container['config'];
diff --git a/system/src/Grav/Common/Service/RequestServiceProvider.php b/system/src/Grav/Common/Service/RequestServiceProvider.php
index d7b496d..6410ef7 100644
--- a/system/src/Grav/Common/Service/RequestServiceProvider.php
+++ b/system/src/Grav/Common/Service/RequestServiceProvider.php
@@ -31,8 +31,8 @@ class RequestServiceProvider implements ServiceProviderInterface
return $creator->fromGlobals();
};
- $container['route'] = function() {
- return Uri::getCurrentRoute();
- };
+ $container['route'] = $container->factory(function() {
+ return clone Uri::getCurrentRoute();
+ });
}
}
diff --git a/system/src/Grav/Common/Service/SessionServiceProvider.php b/system/src/Grav/Common/Service/SessionServiceProvider.php
index 96cbec9..84d23d3 100644
--- a/system/src/Grav/Common/Service/SessionServiceProvider.php
+++ b/system/src/Grav/Common/Service/SessionServiceProvider.php
@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Session;
use Grav\Common\Uri;
+use Grav\Common\Utils;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Session\Message;
@@ -49,14 +50,17 @@ class SessionServiceProvider implements ServiceProviderInterface
// Activate admin if we're inside the admin path.
$is_admin = false;
if ($config->get('plugins.admin.enabled')) {
- $base = '/' . trim($config->get('plugins.admin.route'), '/');
+ $admin_base = '/' . trim($config->get('plugins.admin.route'), '/');
// Uri::route() is not processed yet, let's quickly get what we need.
$current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH));
+ // Test to see if path starts with a supported language + admin base
+ $lang = Utils::pathPrefixedByLangCode($current_route);
+ $lang_admin_base = '/' . $lang . $admin_base;
+
// Check no language, simple language prefix (en) and region specific language prefix (en-US).
- $pos = strpos($current_route, $base);
- if ($pos === 0 || $pos === 3 || $pos === 6) {
+ if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) {
$cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800);
$enabled = $is_admin = true;
}
diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php
index 8c0e08b..6720d46 100644
--- a/system/src/Grav/Common/Themes.php
+++ b/system/src/Grav/Common/Themes.php
@@ -100,7 +100,19 @@ class Themes extends Iterator
}
$theme = $directory->getFilename();
- $result = $this->get($theme);
+
+ try {
+ $result = $this->get($theme);
+ } catch (\Exception $e) {
+ $exception = new \RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e);
+
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage("Theme {$theme} cannot be loaded, please check Exceptions tab", 'error');
+ $debugger->addException($exception);
+
+ continue;
+ }
if ($result) {
$list[$theme] = $result;
@@ -196,8 +208,7 @@ class Themes extends Iterator
foreach ($themeClassFormat as $themeClass) {
if (class_exists($themeClass)) {
- $themeClassName = $themeClass;
- $class = new $themeClassName($grav, $config, $name);
+ $class = new $themeClass($grav, $config, $name);
break;
}
}
diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php
index 4d238da..7bb2bfc 100644
--- a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php
+++ b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php
@@ -41,6 +41,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
->write('$lines = explode("\n", $content);' . PHP_EOL)
->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL)
->write('$content = join("\n", $content);' . PHP_EOL)
- ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($content);' . PHP_EOL);
+ ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL);
}
}
diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php
index baced9a..f84923e 100644
--- a/system/src/Grav/Common/Twig/TwigExtension.php
+++ b/system/src/Grav/Common/Twig/TwigExtension.php
@@ -84,7 +84,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']),
new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']),
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
- new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]),
+ new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']),
new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']),
@@ -455,7 +455,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
}
/**
- * Gets a human readable output for cron sytnax
+ * Gets a human readable output for cron syntax
*
* @param $at
* @return string
@@ -613,12 +613,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/**
* @param string $string
*
+ * @param array $context
* @param bool $block Block or Line processing
* @return mixed|string
*/
- public function markdownFunction($string, $block = true)
+ public function markdownFunction($context, $string, $block = true)
{
- return Utils::processMarkdown($string, $block);
+ $page = $context['page'] ?? null;
+ return Utils::processMarkdown($string, $block, $page);
}
/**
@@ -1004,10 +1006,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/
public function authorize($action)
{
- /** @var UserInterface $user */
- $user = $this->grav['user'];
+ /** @var UserInterface|null $user */
+ $user = $this->grav['user'] ?? null;
- if (!$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
+ if (!$user || !$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
return false;
}
@@ -1136,7 +1138,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
}
/**
- * Get's the Exif data for a file
+ * Get the Exif data for a file
*
* @param string $image
* @param bool $raw
@@ -1154,7 +1156,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$exif_reader = $this->grav['exif']->getReader();
- if ($image & file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
+ if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
$exif_data = $exif_reader->read($image);
diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php
index 3e28280..16d7fb1 100644
--- a/system/src/Grav/Common/Uri.php
+++ b/system/src/Grav/Common/Uri.php
@@ -1154,7 +1154,7 @@ class Uri
$this->scheme = $env['X-FORWARDED-PROTO'];
} elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) {
$this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO'];
- } elseif (isset($env['REQUEST_SCHEME'])) {
+ } elseif (isset($env['REQUEST_SCHEME']) && empty($env['HTTPS'])) {
$this->scheme = $env['REQUEST_SCHEME'];
} else {
$https = $env['HTTPS'] ?? '';
@@ -1286,7 +1286,7 @@ class Uri
}
/**
- * Get's post from either $_POST or JSON response object
+ * Get post from either $_POST or JSON response object
* By default returns all data, or can return a single item
*
* @param string $element
@@ -1345,7 +1345,7 @@ class Uri
*/
public function isValidExtension($extension)
{
- $valid_page_types = implode('|', Grav::instance()['config']->get('system.pages.types'));
+ $valid_page_types = implode('|', Utils::getSupportPageTypes());
// Strip the file extension for valid page types
if (preg_match('/(' . $valid_page_types . ')/', $extension)) {
diff --git a/system/src/Grav/Common/User/DataUser/UserCollection.php b/system/src/Grav/Common/User/DataUser/UserCollection.php
index 0970ce7..800322a 100644
--- a/system/src/Grav/Common/User/DataUser/UserCollection.php
+++ b/system/src/Grav/Common/User/DataUser/UserCollection.php
@@ -118,4 +118,13 @@ class UserCollection implements UserCollectionInterface
return $file_path && unlink($file_path);
}
+
+ public function count(): int
+ {
+ // check for existence of a user account
+ $account_dir = $file_path = Grav::instance()['locator']->findResource('account://');
+ $accounts = glob($account_dir . '/*.yaml') ?: [];
+
+ return count($accounts);
+ }
}
diff --git a/system/src/Grav/Common/User/FlexUser/User.php b/system/src/Grav/Common/User/FlexUser/User.php
index 5ef3819..0d83c51 100644
--- a/system/src/Grav/Common/User/FlexUser/User.php
+++ b/system/src/Grav/Common/User/FlexUser/User.php
@@ -9,6 +9,7 @@
namespace Grav\Common\User\FlexUser;
+use Grav\Common\Data\Blueprint;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media;
@@ -21,6 +22,7 @@ use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject;
+use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Form\FormFlashFile;
@@ -381,6 +383,31 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return $this->getBlueprint()->extra($this->toArray());
}
+ /**
+ * @param string $name
+ * @return Blueprint
+ */
+ public function getBlueprint(string $name = '')
+ {
+ $blueprint = clone parent::getBlueprint($name);
+
+ $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
+ $params = (array)$call['params'];
+ $method = array_shift($params);
+
+ if (method_exists($this, $method)) {
+ $value = $this->{$method}(...$params);
+ if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
+ $field[$property] = array_merge_recursive($field[$property], $value);
+ } else {
+ $field[$property] = $value;
+ }
+ }
+ });
+
+ return $blueprint->init();
+ }
+
/**
* Return unmodified data as raw string.
*
@@ -420,6 +447,15 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
*/
public function save()
{
+ // TODO: We may want to handle this in the storage layer in the future.
+ $key = $this->getStorageKey();
+ if (!$key || strpos($key, '@@')) {
+ $storage = $this->getFlexDirectory()->getStorage();
+ if ($storage instanceof FileStorage) {
+ $this->setStorageKey($this->getKey());
+ }
+ }
+
$password = $this->getProperty('password');
if (null !== $password) {
$this->unsetProperty('password');
@@ -431,6 +467,20 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return parent::save();
}
+ public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool
+ {
+ if (null === $user) {
+ /** @var UserInterface $user */
+ $user = Grav::instance()['user'] ?? null;
+ }
+
+ if ($user instanceof User && $user->getStorageKey() === $this->getStorageKey()) {
+ return true;
+ }
+
+ return parent::isAuthorized($action, $scope, $user);
+ }
+
/**
* @return array
*/
diff --git a/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php b/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php
index a8cc7d6..37b9009 100644
--- a/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php
+++ b/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php
@@ -9,7 +9,7 @@
namespace Grav\Common\User\Interfaces;
-interface UserCollectionInterface
+interface UserCollectionInterface extends \Countable
{
/**
* Load user account.
diff --git a/system/src/Grav/Common/User/Traits/UserTrait.php b/system/src/Grav/Common/User/Traits/UserTrait.php
index 2b69768..f711cc9 100644
--- a/system/src/Grav/Common/User/Traits/UserTrait.php
+++ b/system/src/Grav/Common/User/Traits/UserTrait.php
@@ -148,12 +148,13 @@ trait UserTrait
// Try looking for provider.
$provider = $this->get('provider');
- if (\is_array($provider)) {
- if (isset($provider['avatar_url']) && \is_string($provider['avatar_url'])) {
- return $provider['avatar_url'];
+ $provider_options = $this->get($provider);
+ if (\is_array($provider_options)) {
+ if (isset($provider_options['avatar_url']) && \is_string($provider_options['avatar_url'])) {
+ return $provider_options['avatar_url'];
}
- if (isset($provider['avatar']) && \is_string($provider['avatar'])) {
- return $provider['avatar'];
+ if (isset($provider_options['avatar']) && \is_string($provider_options['avatar'])) {
+ return $provider_options['avatar'];
}
}
diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php
index c254009..56b2d89 100644
--- a/system/src/Grav/Common/Utils.php
+++ b/system/src/Grav/Common/Utils.php
@@ -13,6 +13,7 @@ use Grav\Common\Helpers\Truncator;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
+use Grav\Common\Page\Markdown\Excerpts;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -27,55 +28,103 @@ abstract class Utils
/**
* Simple helper method to make getting a Grav URL easier
*
- * @param string $input
+ * @param string|object $input
* @param bool $domain
+ * @param bool $fail_gracefully
* @return bool|null|string
*/
- public static function url($input, $domain = false)
+ public static function url($input, $domain = false, $fail_gracefully = false)
{
- if (!trim((string)$input)) {
- $input = '/';
+ if ((!is_string($input) && !method_exists($input, '__toString')) || !trim($input)) {
+ if ($fail_gracefully) {
+ $input = '/';
+ } else {
+ return false;
+ }
}
- if (Grav::instance()['config']->get('system.absolute_urls', false)) {
- $domain = true;
- }
+ $input = (string)$input;
- if (Grav::instance()['uri']->isExternal($input)) {
+ if (Uri::isExternal($input)) {
return $input;
}
+ $grav = Grav::instance();
+
/** @var Uri $uri */
- $uri = Grav::instance()['uri'];
+ $uri = $grav['uri'];
- $root = $uri->rootUrl();
- $input = Utils::replaceFirstOccurrence($root, '', $input);
-
- $input = ltrim((string)$input, '/');
-
- if (Utils::contains((string)$input, '://')) {
+ if (static::contains((string)$input, '://')) {
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
+ $locator = $grav['locator'];
$parts = Uri::parseUrl($input);
- if ($parts) {
- $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
+ if (is_array($parts)) {
+ // Make sure we always have scheme, host, port and path.
+ $scheme = $parts['scheme'] ?? '';
+ $host = $parts['host'] ?? '';
+ $port = $parts['port'] ?? '';
+ $path = $parts['path'] ?? '';
- if (isset($parts['query'])) {
- $resource = $resource . '?' . $parts['query'];
+ if ($scheme && !$port) {
+ // If URL has a scheme, we need to check if it's one of Grav streams.
+ if (!$locator->schemeExists($scheme)) {
+ // If scheme does not exists as a stream, assume it's external.
+ return str_replace(' ', '%20', $input);
+ }
+
+ // Attempt to find the resource (because of parse_url() we need to put host back to path).
+ $resource = $locator->findResource("{$scheme}://{$host}{$path}", false);
+
+ if ($resource === false) {
+ if (!$fail_gracefully) {
+ return false;
+ }
+
+ // Return location where the file would be if it was saved.
+ $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true);
+ }
+
+ } elseif ($host || $port) {
+ // If URL doesn't have scheme but has host or port, it is external.
+ return str_replace(' ', '%20', $input);
}
+
+ if (!empty($resource)) {
+ // Add query string back.
+ if (isset($parts['query'])) {
+ $resource .= '?' . $parts['query'];
+ }
+
+ // Add fragment back.
+ if (isset($parts['fragment'])) {
+ $resource .= '#' . $parts['fragment'];
+ }
+ }
+
} else {
// Not a valid URL (can still be a stream).
$resource = $locator->findResource($input, false);
}
-
} else {
+ $root = $uri->rootUrl();
+
+ if (static::startsWith($input, $root)) {
+ $input = static::replaceFirstOccurrence($root, '', $input);
+ }
+
+ $input = ltrim($input, '/');
+
$resource = $input;
}
+ if (!$fail_gracefully && $resource === false) {
+ return false;
+ }
+ $domain = $domain ?: $grav['config']->get('system.absolute_urls', false);
return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
}
@@ -274,6 +323,35 @@ abstract class Utils
return (object)array_merge((array)$obj1, (array)$obj2);
}
+ /**
+ * Lowercase an entire array. Useful when combined with `in_array()`
+ *
+ * @param array $a
+ * @return array|false
+ */
+ public static function arrayLower(Array $a)
+ {
+ return array_map('mb_strtolower', $a);
+ }
+
+ /**
+ * Simple function to remove item/s in an array by value
+ *
+ * @param $search array
+ * @param $value string|array
+ * @return array
+ */
+ public static function arrayRemoveValue(Array $search, $value)
+ {
+ foreach ((array) $value as $val) {
+ $key = array_search($val, $search);
+ if ($key !== false) {
+ unset($search[$key]);
+ }
+ }
+ return $search;
+ }
+
/**
* Recursive Merge with uniqueness
*
@@ -974,17 +1052,19 @@ abstract class Utils
*
* @param string $string The path
*
- * @return bool
+ * @return bool|string Either false or the language
+ *
*/
public static function pathPrefixedByLangCode($string)
{
- if (strlen($string) <= 3) {
- return false;
+ $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
+ $parts = explode('/', trim($string, '/'));
+
+ if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
+ return $parts[0];
}
- $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
-
- return $string[0] === '/' && $string[3] === '/' && \in_array(substr($string, 1, 2), $languages_enabled, true);
+ return false;
}
/**
@@ -1054,12 +1134,9 @@ abstract class Utils
*/
private static function generateNonceString($action, $previousTick = false)
{
- $username = '';
- if (isset(Grav::instance()['user'])) {
- $user = Grav::instance()['user'];
- $username = $user->username;
- }
+ $grav = Grav::instance();
+ $username = isset($grav['user']) ? $grav['user']->username : '';
$token = session_id();
$i = self::nonceTick();
@@ -1067,7 +1144,7 @@ abstract class Utils
$i--;
}
- return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
+ return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt'));
}
/**
@@ -1281,7 +1358,7 @@ abstract class Utils
}
/**
- * Get's path based on a token
+ * Get path based on a token
*
* @param string $path
* @param PageInterface|null $page
@@ -1341,6 +1418,8 @@ abstract class Utils
$post_max_size = static::parseSize(ini_get('post_max_size'));
if ($post_max_size > 0) {
$max_size = $post_max_size;
+ } else {
+ $max_size = 0;
}
$upload_max = static::parseSize(ini_get('upload_max_filesize'));
@@ -1388,7 +1467,7 @@ abstract class Utils
$pow = min($pow, count($units) - 1);
// Uncomment one of the following alternatives
- $bytes /= pow(1024, $pow);
+ $bytes /= 1024 ** $pow;
// $bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
@@ -1404,11 +1483,12 @@ abstract class Utils
{
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size);
+
if ($unit) {
- return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
- } else {
- return round($size);
+ $size = $size * pow(1024, stripos('bkmgtpezy', $unit[0]));
}
+
+ return (int) abs(round($size));
}
/**
@@ -1446,19 +1526,28 @@ abstract class Utils
*
* @param string $string
*
- * @param bool $block Block or Line processing
+ * @param bool $block Block or Line processing
+ * @param null $page
* @return string
+ * @throws \Exception
*/
- public static function processMarkdown($string, $block = true)
+ public static function processMarkdown($string, $block = true, $page = null)
{
- $page = Grav::instance()['page'] ?? null;
- $defaults = Grav::instance()['config']->get('system.pages.markdown');
+ $grav = Grav::instance();
+ $page = $page ?? $grav['page'] ?? null;
+ $defaults = [
+ 'markdown' => $grav['config']->get('system.pages.markdown', []),
+ 'images' => $grav['config']->get('system.images', [])
+ ];
+ $extra = $defaults['markdown']['extra'] ?? false;
+
+ $excerpts = new Excerpts($page, $defaults);
// Initialize the preferred variant of Parsedown
- if ($defaults['extra']) {
- $parsedown = new ParsedownExtra($page, $defaults);
+ if ($extra) {
+ $parsedown = new ParsedownExtra($excerpts);
} else {
- $parsedown = new Parsedown($page, $defaults);
+ $parsedown = new Parsedown($excerpts);
}
if ($block) {
@@ -1477,12 +1566,11 @@ abstract class Utils
* @param int $prefix
*
* @return string
- * @throws \InvalidArgumentException if provided an invalid IP
*/
public static function getSubnet($ip, $prefix = 64)
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
- throw new \InvalidArgumentException('Invalid IP: ' . $ip);
+ return $ip;
}
// Packed representation of IP
@@ -1509,4 +1597,23 @@ abstract class Utils
return $subnet;
}
+
+ /**
+ * Wrapper to ensure html, htm in the front of the supported page types
+ *
+ * @param array|null $defaults
+ * @return array|mixed
+ */
+ public static function getSupportPageTypes(array $defaults = null)
+ {
+ $types = Grav::instance()['config']->get('system.pages.types', $defaults);
+
+ // remove html/htm
+ $types = static::arrayRemoveValue($types, ['html', 'htm']);
+
+ // put them back at the front
+ $types = array_merge(['html', 'htm'], $types);
+
+ return $types;
+ }
}
diff --git a/system/src/Grav/Console/Cli/ClearCacheCommand.php b/system/src/Grav/Console/Cli/ClearCacheCommand.php
index 0ab4128..8ef6f4f 100644
--- a/system/src/Grav/Console/Cli/ClearCacheCommand.php
+++ b/system/src/Grav/Console/Cli/ClearCacheCommand.php
@@ -21,6 +21,7 @@ class ClearCacheCommand extends ConsoleCommand
->setName('cache')
->setAliases(['clearcache', 'cache-clear'])
->setDescription('Clears Grav cache')
+ ->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files')
->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches')
->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches')
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
@@ -64,6 +65,8 @@ class ClearCacheCommand extends ConsoleCommand
$remove = 'cache-only';
} elseif ($this->input->getOption('tmp-only')) {
$remove = 'tmp-only';
+ } elseif ($this->input->getOption('invalidate')) {
+ $remove = 'invalidate';
} else {
$remove = 'standard';
}
diff --git a/system/src/Grav/Console/Cli/YamlLinterCommand.php b/system/src/Grav/Console/Cli/YamlLinterCommand.php
index 18cdb49..894f1c0 100644
--- a/system/src/Grav/Console/Cli/YamlLinterCommand.php
+++ b/system/src/Grav/Console/Cli/YamlLinterCommand.php
@@ -61,6 +61,15 @@ class YamlLinterCommand extends ConsoleCommand
$this->displayErrors($errors, $io);
}
+ $io->section('Page Blueprints');
+ $errors = YamlLinter::lintBlueprints();
+
+ if (empty($errors)) {
+ $io->success('No YAML Linting issues with blueprints');
+ } else {
+ $this->displayErrors($errors, $io);
+ }
+
}
protected function displayErrors($errors, $io)
diff --git a/system/src/Grav/Console/Gpm/InfoCommand.php b/system/src/Grav/Console/Gpm/InfoCommand.php
index ee89584..2fe1a2e 100644
--- a/system/src/Grav/Console/Gpm/InfoCommand.php
+++ b/system/src/Grav/Console/Gpm/InfoCommand.php
@@ -114,7 +114,7 @@ class InfoCommand extends ConsoleCommand
if ($info === 'date') {
$name = 'Last Update';
- $data = date('D, j M Y, H:i:s, P ', strtotime('2014-09-16T00:07:16Z'));
+ $data = date('D, j M Y, H:i:s, P ', strtotime($data));
}
$name = str_pad($name, 12);
diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php
index 840ee5b..8d544d4 100644
--- a/system/src/Grav/Console/Gpm/InstallCommand.php
+++ b/system/src/Grav/Console/Gpm/InstallCommand.php
@@ -372,7 +372,7 @@ class InstallCommand extends ConsoleCommand
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
- $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
+ $answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" '- + {{ page.content|raw }} +
+{{ feature.text }}
+ {% endif %} ++ + +
+