diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcd4bf..c274304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# v1.7.9 +## 03/19/2021 + +1. [](#new) + * Added `Media::hide()` method to hide files from media + * Added `Utils::getPathFromToken()` method which works also with `Flex Objects` + * Added `FlexMediaTrait::getMediaField()`, which can be used to access custom media set in the blueprint fields + * Added `FlexMediaTrait::getFieldSettings()`, which can be used to get media field settings +1. [](#improved) + * Method `Utils::getPagePathFromToken()` now calls the more generic `Utils::getPathFromToken()` + * Updated `SECURITY.md` to use security@getgrav.org +1. [](#bugfix) + * Fixed broken media upload in `Flex` with `@self/path`, `@page` and `@theme` destinations [#3275](https://github.com/getgrav/grav/issues/3275) + * Fixed media fields excluding newly deleted files before saving the object + * Fixed method `$pages->find()` should never redirect [#3266](https://github.com/getgrav/grav/pull/3266) + * Fixed `Page::activeChild()` throwing an error [#3276](https://github.com/getgrav/grav/issues/3276) + * Fixed `Flex Page` CRUD ACL when creating a new page (needs Flex Objects plugin update) [grav-plugin-flex-objects#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115) + * Fixed the list of pages not showing up in admin [#3280](https://github.com/getgrav/grav/issues/3280) + * Fixed text field min/max validation for UTF8 characters [#3281](https://github.com/getgrav/grav/issues/3281) + * Fixed redirects using wrong redirect code + # v1.7.8 ## 03/17/2021 diff --git a/SECURITY.md b/SECURITY.md index e2d0cfd..30830c7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,4 +18,4 @@ If you cannot update to the latest stable version available because, for example ## Reporting a Vulnerability -Please contact contact@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible. +Please contact security@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible. diff --git a/composer.lock b/composer.lock index 6ea862f..13fa182 100644 --- a/composer.lock +++ b/composer.lock @@ -381,16 +381,16 @@ }, { "name": "donatj/phpuseragentparser", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/donatj/PhpUserAgent.git", - "reference": "f9a521726b2ce4c5173281ceaab5a02c05b691ef" + "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/f9a521726b2ce4c5173281ceaab5a02c05b691ef", - "reference": "f9a521726b2ce4c5173281ceaab5a02c05b691ef", + "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/246c1cf0a44f07168c702203bf30d5f48f17bab0", + "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0", "shasum": "" }, "require": { @@ -433,7 +433,7 @@ ], "support": { "issues": "https://github.com/donatj/PhpUserAgent/issues", - "source": "https://github.com/donatj/PhpUserAgent/tree/v1.3.0" + "source": "https://github.com/donatj/PhpUserAgent/tree/v1.4.0" }, "funding": [ { @@ -445,7 +445,7 @@ "type": "github" } ], - "time": "2021-02-18T04:30:49+00:00" + "time": "2021-03-16T16:25:14+00:00" }, { "name": "dragonmantank/cron-expression", @@ -642,16 +642,16 @@ }, { "name": "filp/whoops", - "version": "2.9.2", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "df7933820090489623ce0be5e85c7e693638e536" + "reference": "6ecda5217bf048088b891f7403b262906be5a957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536", - "reference": "df7933820090489623ce0be5e85c7e693638e536", + "url": "https://api.github.com/repos/filp/whoops/zipball/6ecda5217bf048088b891f7403b262906be5a957", + "reference": "6ecda5217bf048088b891f7403b262906be5a957", "shasum": "" }, "require": { @@ -701,7 +701,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.9.2" + "source": "https://github.com/filp/whoops/tree/2.10.0" }, "funding": [ { @@ -709,7 +709,7 @@ "type": "github" } ], - "time": "2021-01-24T12:00:00+00:00" + "time": "2021-03-16T12:00:00+00:00" }, { "name": "gregwar/cache", @@ -4929,16 +4929,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.2", + "version": "9.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4" + "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f661659747f2f87f9e72095bb207bceb0f151cb4", - "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/27241ac75fc37ecf862b6e002bf713b6566cbe41", + "reference": "27241ac75fc37ecf862b6e002bf713b6566cbe41", "shasum": "" }, "require": { @@ -5016,7 +5016,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.3" }, "funding": [ { @@ -5028,7 +5028,7 @@ "type": "github" } ], - "time": "2021-02-02T14:45:58+00:00" + "time": "2021-03-17T07:30:34+00:00" }, { "name": "psr/http-client", diff --git a/system/defines.php b/system/defines.php index 6c4b812..e7f365a 100644 --- a/system/defines.php +++ b/system/defines.php @@ -8,7 +8,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.7.8'); +define('GRAV_VERSION', '1.7.9'); define('GRAV_SCHEMA', '1.7.0_2020-11-20_1'); define('GRAV_TESTING', false); diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 3e0ba32..4cba620 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -27,7 +27,6 @@ use function is_bool; use function is_float; use function is_int; use function is_string; -use function strlen; /** * Class Validation @@ -239,16 +238,20 @@ class Validation $value = trim($value); } - if (isset($params['min']) && strlen($value) < $params['min']) { + $len = mb_strlen($value); + + $min = (int)($params['min'] ?? 0); + if ($min && $len < $min) { return false; } - if (isset($params['max']) && strlen($value) > $params['max']) { + $max = (int)($params['max'] ?? 0); + if ($max && $len > $max) { return false; } - $min = $params['min'] ?? 0; - if (isset($params['step']) && (strlen($value) - $min) % $params['step'] === 0) { + $step = (int)($params['step'] ?? 0); + if ($step && ($len - $min) % $step === 0) { return false; } @@ -271,11 +274,13 @@ class Validation return ''; } + $value = (string)$value; + if (!empty($params['trim'])) { $value = trim($value); } - return (string) $value; + return $value; } /** @@ -332,7 +337,7 @@ class Validation */ protected static function filterLower($value, array $params) { - return strtolower($value); + return mb_strtolower($value); } /** @@ -342,7 +347,7 @@ class Validation */ protected static function filterUpper($value, array $params) { - return strtoupper($value); + return mb_strtoupper($value); } @@ -534,7 +539,7 @@ class Validation */ protected static function filterNumber($value, array $params, array $field) { - return (string)(int)$value !== (string)(float)$value ? (float) $value : (int) $value; + return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value; } /** diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index c5b7ce9..d53d7f8 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -522,12 +522,13 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $tmp = $child->children()->getIndex(); $child_count = $tmp->count(); $count = $filters ? $tmp->filterBy($filters, true)->count() : null; + $route = $child->getRoute(); $payload = [ 'item-key' => basename($child->rawRoute() ?? $child->getKey()), 'icon' => $icon, 'title' => htmlspecialchars($child->menu()), 'route' => [ - 'display' => $child->getRoute()->toString(false) ?: '/', + 'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '', 'raw' => $child->rawRoute(), ], 'modified' => $this->jsDate($child->modified()), diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php index 2d45b91..9ed8b67 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php @@ -22,6 +22,7 @@ use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait; use Grav\Common\Language\Language; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\Utils; use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Flex\FlexObject; @@ -321,6 +322,28 @@ class PageObject extends FlexPageObject return $this; } + /** + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool + { + // Special case: creating a new page means checking parent for its permissions. + if ($action === 'create' && !$this->exists()) { + $parent = $this->parent(); + if ($parent && method_exists($parent, 'isAuthorized')) { + return $parent->isAuthorized($action, $scope, $user); + } + + return false; + } + + return parent::isAuthorizedOverride($user, $action, $scope, $isMe); + } + /** * @param array $ordering * @return PageCollection|null diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php index 341ac4d..3b490a5 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php @@ -15,6 +15,7 @@ use Grav\Common\Grav; use Grav\Common\Page\Interfaces\PageCollectionInterface; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\Uri; use Grav\Common\Utils; use Grav\Framework\Filesystem\Filesystem; use RuntimeException; @@ -97,6 +98,7 @@ trait PageRoutableTrait public function activeChild(): bool { $grav = Grav::instance(); + /** @var Uri $uri */ $uri = $grav['uri']; /** @var Pages $pages */ $pages = $grav['pages']; diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index d9ad289..c9097ee 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -426,7 +426,7 @@ class Grav extends Container // Clean route for redirect $route = preg_replace("#^\/[\\\/]+\/#", '/', $route); - if (null !== $code || $code < 300 || $code > 399) { + if ($code < 300 || $code > 399) { $code = null; } diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index 06b44a7..db81ddd 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -198,6 +198,17 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac } } + /** + * @param string $name + * @return void + */ + public function hide($name) + { + $this->offsetUnset($name); + + unset($this->images[$name], $this->videos[$name], $this->audios[$name], $this->files[$name]); + } + /** * Create Medium from a file. * diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index baac905..5c92cba 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -2496,21 +2496,23 @@ class Page implements PageInterface */ public function activeChild() { - $uri = Grav::instance()['uri']; - $pages = Grav::instance()['pages']; + $grav = Grav::instance(); + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Pages $pages */ + $pages = $grav['pages']; $uri_path = rtrim(urldecode($uri->path()), '/'); - $routes = Grav::instance()['pages']->routes(); + $routes = $pages->routes(); if (isset($routes[$uri_path])) { + $page = $pages->find($uri->route()); /** @var PageInterface|null $child_page */ - $child_page = $pages->find($uri->route())->parent(); - if ($child_page) { - while (!$child_page->root()) { - if ($this->path() === $child_page->path()) { - return true; - } - $child_page = $child_page->parent(); + $child_page = $page ? $page->parent() : null; + while ($child_page && !$child_page->root()) { + if ($this->path() === $child_page->path()) { + return true; } + $child_page = $child_page->parent(); } } diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 4f64502..3b3ce8e 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -880,103 +880,146 @@ class Pages } /** - * alias method to return find a page. + * Find a page based on route. * - * @param string $route The relative URL of the page - * @param bool $all + * @param string $route The route of the page + * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable * @return PageInterface|null */ public function find($route, $all = false) { - return $this->dispatch($route, $all, false); + $route = urldecode((string)$route); + + // Fetch page if there's a defined route to it. + $path = $this->routes[$route] ?? null; + $page = null !== $path ? $this->get($path) : null; + + // Try without trailing slash + if (null === $page && Utils::endsWith($route, '/')) { + $path = $this->routes[rtrim($route, '/')] ?? null; + $page = null !== $path ? $this->get($path) : null; + } + + if (!$all && !isset($this->grav['admin'])) { + if (null === $page || !$page->routable()) { + // If the page cannot be accessed, look for the site wide routes and wildcards. + $page = $this->findSiteBasedRoute($route) ?? $page; + } + } + + return $page; + } + + /** + * Check site based routes. + * + * @param string $route + * @return PageInterface|null + */ + protected function findSiteBasedRoute($route) + { + /** @var Config $config */ + $config = $this->grav['config']; + + $site_routes = $config->get('site.routes'); + if (!is_array($site_routes)) { + return null; + } + + $page = null; + + // See if route matches one in the site configuration + $site_route = $site_routes[$route] ?? null; + if ($site_route) { + $page = $this->find($site_route); + } else { + // Use reverse order because of B/C (previously matched multiple and returned the last match). + foreach (array_reverse($site_routes, true) as $pattern => $replace) { + $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#'; + try { + $found = preg_replace($pattern, $replace, $route); + if ($found && $found !== $route) { + $page = $this->find($found); + if ($page) { + return $page; + } + } + } catch (ErrorException $e) { + $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage()); + } + } + } + + return $page; } /** * Dispatch URI to a page. * * @param string $route The relative URL of the page - * @param bool $all - * @param bool $redirect + * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable + * @param bool $redirect If true, allow redirects * @return PageInterface|null * @throws Exception */ public function dispatch($route, $all = false, $redirect = true) { - $route = urldecode($route); + $page = $this->find($route, true); - // Fetch page if there's a defined route to it. - $path = $this->routes[$route] ?? null; - $page = null !== $path ? $this->get($path) : null; - // Try without trailing slash - if (!$page && Utils::endsWith($route, '/')) { - $path = $this->routes[rtrim($route, '/')] ?? null; - $page = null !== $path ? $this->get($path) : null; + // If we want all pages or are in admin, return what we already have. + if ($all || isset($this->grav['admin'])) { + return $page; } - // Are we in the admin? this is important! - $not_admin = !isset($this->grav['admin']); - - // If the page cannot be reached, look into site wide redirects, routes + wildcards - if (!$all && $not_admin) { - // If the page is a simple redirect, just do it. - if ($redirect && $page && $page->redirect()) { - $this->grav->redirectLangSafe($page->redirect()); - } - - // fall back and check site based redirects - if (!$page || !$page->routable()) { - // Redirect to the first child (placeholder page) - if ($redirect && $page && count($children = $page->children()->visible()->routable()->published()) > 0) { - $this->grav->redirectLangSafe($children->first()->route()); + if ($page) { + $routable = $page->routable(); + if ($redirect) { + if ($page->redirect()) { + // Follow a redirect page. + $this->grav->redirectLangSafe($page->redirect()); } - /** @var Config $config */ - $config = $this->grav['config']; + if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) { + // Redirect to the first visible child as current page isn't routable. + $this->grav->redirectLangSafe($child->route()); + } + } - // See if route matches one in the site configuration - $site_route = $config->get("site.routes.{$route}"); - if ($site_route) { - $page = $this->dispatch($site_route, $all, $redirect); - } else { - /** @var Uri $uri */ - $uri = $this->grav['uri']; - /** @var \Grav\Framework\Uri\Uri $source_url */ - $source_url = $uri->uri(false); + if ($routable) { + return $page; + } + } - // Try Regex style redirects - $site_redirects = $config->get('site.redirects'); - if (is_array($site_redirects)) { - foreach ((array)$site_redirects as $pattern => $replace) { - $pattern = ltrim($pattern, '^'); - $pattern = '#^' . str_replace('/', '\/', $pattern) . '#'; - try { - /** @var string $found */ - $found = preg_replace($pattern, $replace, $source_url); - if ($found && $found !== $source_url) { - $this->grav->redirectLangSafe($found); - } - } catch (ErrorException $e) { - $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage()); - } - } - } - - // Try Regex style routes - $site_routes = $config->get('site.routes'); - if (is_array($site_routes)) { - foreach ((array)$site_routes as $pattern => $replace) { - $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#'; - try { - /** @var string $found */ - $found = preg_replace($pattern, $replace, $source_url); - if ($found && $found !== $source_url) { - $page = $this->dispatch($found, $all, $redirect); - } - } catch (ErrorException $e) { - $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage()); - } - } + $route = urldecode((string)$route); + + // The page cannot be reached, look into site wide redirects, routes and wildcards. + $redirectedPage = $this->findSiteBasedRoute($route); + if ($redirectedPage) { + $page = $this->dispatch($redirectedPage->route(), false, $redirect); + } + + /** @var Config $config */ + $config = $this->grav['config']; + + /** @var Uri $uri */ + $uri = $this->grav['uri']; + /** @var \Grav\Framework\Uri\Uri $source_url */ + $source_url = $uri->uri(false); + + // Try Regex style redirects + $site_redirects = $config->get('site.redirects'); + if (is_array($site_redirects)) { + foreach ((array)$site_redirects as $pattern => $replace) { + $pattern = ltrim($pattern, '^'); + $pattern = '#^' . str_replace('/', '\/', $pattern) . '#'; + try { + /** @var string $found */ + $found = preg_replace($pattern, $replace, $source_url); + if ($found && $found !== $source_url) { + $this->grav->redirectLangSafe($found); } + } catch (ErrorException $e) { + $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage()); } } } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 22f6b86..f704608 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -519,7 +519,7 @@ class Uri * Return the full uri * * @param bool $include_root - * @return mixed + * @return string */ public function uri($include_root = true) { diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index a076f0a..dac5cf7 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -12,11 +12,15 @@ namespace Grav\Common; use DateTime; use DateTimeZone; use Exception; +use Grav\Common\Flex\Types\Pages\PageObject; 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 Grav\Common\Page\Pages; +use Grav\Framework\Flex\Flex; +use Grav\Framework\Flex\Interfaces\FlexObjectInterface; use InvalidArgumentException; use Negotiation\Accept; use Negotiation\Negotiator; @@ -1520,7 +1524,7 @@ abstract class Utils } /** - * Get path based on a token + * Get relative page path based on a token. * * @param string $path * @param PageInterface|null $page @@ -1529,47 +1533,122 @@ abstract class Utils */ public static function getPagePathFromToken($path, PageInterface $page = null) { - $path_parts = pathinfo($path); - $grav = Grav::instance(); + return static::getPathFromToken($path, $page); + } - $basename = ''; - if (isset($path_parts['extension'])) { - $basename = '/' . $path_parts['basename']; - $path = rtrim($path_parts['dirname'], ':'); + /** + * Get relative path based on a token. + * + * Path supports following syntaxes: + * + * 'self@', 'self@/path' + * 'page@:/route', 'page@:/route/filename.ext' + * 'theme@:', 'theme@:/path' + * + * @param string $path + * @param FlexObjectInterface|PageInterface|null $object + * @return string + * @throws RuntimeException + */ + public static function getPathFromToken($path, $object = null) + { + $matches = static::resolveTokenPath($path); + if (null === $matches) { + return $path; } - $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/'; - preg_match($regex, $path, $matches); + $grav = Grav::instance(); - if ($matches) { - if ($matches[1]) { - if (null === $page) { - throw new RuntimeException('Page not available for this self@ reference'); + switch ($matches[0]) { + case 'self': + if (null === $object) { + throw new RuntimeException(sprintf('Page not available for self@ reference: %s', $path)); } - } elseif ($matches[2]) { - // page@ - $parts = explode(':', $path); - $route = $parts[1]; - $page = $grav['page']->find($route); - } elseif ($matches[3]) { - // theme@ - $parts = explode(':', $path); - $route = $parts[1]; - $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource('theme://')); - return $theme . $route . $basename; + if ($matches[2] === '') { + if ($object->exists()) { + $route = '/' . $matches[1]; + + if ($object instanceof PageInterface) { + return trim($object->relativePagePath() . $route, '/'); + } + + $folder = $object->getMediaFolder(); + if ($folder) { + return trim($folder . $route, '/'); + } + } else { + return ''; + } + } + + break; + case 'page': + if ($matches[1] === '') { + $route = '/' . $matches[2]; + + // Exclude filename from the page lookup. + if (pathinfo($route, PATHINFO_EXTENSION)) { + $basename = '/' . basename($route); + $route = \dirname($route); + } else { + $basename = ''; + } + + $key = trim($route === '/' ? $grav['config']->get('system.home.alias') : $route, '/'); + if ($object instanceof PageObject) { + $object = $object->getFlexDirectory()->getObject($key); + } elseif (static::isAdminPlugin()) { + /** @var Flex|null $flex */ + $flex = $grav['flex'] ?? null; + $object = $flex ? $flex->getObject($key, 'pages') : null; + } else { + /** @var Pages $pages */ + $pages = $grav['pages']; + $object = $pages->find($route); + } + + if ($object instanceof PageInterface) { + return trim($object->relativePagePath() . $basename, '/'); + } + } + + break; + case 'theme': + if ($matches[1] === '') { + $route = '/' . $matches[2]; + $theme = $grav['locator']->findResource('theme://', false); + if (false !== $theme) { + return trim($theme . $route, '/'); + } + } + + break; + } + + throw new RuntimeException(sprintf('Token path not found: %s', $path)); + } + + /** + * Returns [token, route, path] from '@token/route:/path'. Route and path are optional. If pattern does not match, return null. + * + * @param string $path + * @return string[]|null + */ + private static function resolveTokenPath(string $path): ?array + { + if (strpos($path, '@') !== false) { + $regex = '/^(@\w+|\w+@|@\w+@)([^:]*)(.*)$/u'; + if (preg_match($regex, $path, $matches)) { + return [ + trim($matches[1], '@'), + trim($matches[2], '/'), + trim($matches[3], ':/') + ]; } - } else { - return $path . $basename; } - if (!$page) { - throw new RuntimeException('Page route not found: ' . $path); - } - - $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path); - - return $path . $basename; + return null; } /** diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php index b2be4ee..90773cd 100644 --- a/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php @@ -420,9 +420,11 @@ trait PageRoutableTrait return $this->_parentCache; } + // Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'. + $filesystem = Filesystem::getInstance(false); $directory = $this->getFlexDirectory(); - $parentKey = ltrim(dirname("/{$this->getKey()}"), '/'); - if ($parentKey) { + $parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/'); + if ('' !== $parentKey) { $parent = $directory->getObject($parentKey); $language = $this->getLanguage(); if ($language && $parent && method_exists($parent, 'getTranslation')) { @@ -433,7 +435,7 @@ trait PageRoutableTrait } else { $index = $directory->getIndex(); - $this->_parentCache = method_exists($index, 'getRoot') ? $index->getRoot() : null; + $this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null; } return $this->_parentCache; @@ -497,22 +499,22 @@ trait PageRoutableTrait public function activeChild(): bool { $grav = Grav::instance(); + /** @var Uri $uri */ $uri = $grav['uri']; + /** @var Pages $pages */ $pages = $grav['pages']; $uri_path = rtrim(urldecode($uri->path()), '/'); $routes = $pages->routes(); if (isset($routes[$uri_path])) { - /** @var PageInterface $child_page|null */ - $child_page = $pages->find($uri->route())->parent(); - if (null !== $child_page) { - while (!$child_page->root()) { - if ($this->path() === $child_page->path()) { - return true; - } - /** @var PageInterface $child_page|null */ - $child_page = $child_page->parent(); + $page = $pages->find($uri->route()); + /** @var PageInterface|null $child_page */ + $child_page = $page ? $page->parent() : null; + while ($child_page && !$child_page->root()) { + if ($this->path() === $child_page->path()) { + return true; } + $child_page = $child_page->parent(); } } diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php index c03a23d..c9ae580 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -14,8 +14,10 @@ use Grav\Common\Grav; use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaUploadInterface; use Grav\Common\Media\Traits\MediaTrait; +use Grav\Common\Page\Media; use Grav\Common\Page\Medium\Medium; use Grav\Common\Page\Medium\MediumFactory; +use Grav\Common\Utils; use Grav\Framework\Cache\CacheInterface; use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Flex\FlexDirectory; @@ -23,6 +25,7 @@ use Grav\Framework\Form\FormFlashFile; use Psr\Http\Message\UploadedFileInterface; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RuntimeException; +use function array_key_exists; use function in_array; use function is_array; use function is_callable; @@ -75,11 +78,40 @@ trait FlexMediaTrait return $media; } + /** + * @param string $field + * @return MediaCollectionInterface|null + */ + public function getMediaField(string $field): ?MediaCollectionInterface + { + // Field specific media. + $settings = $this->getFieldSettings($field); + if (!empty($settings['media_field'])) { + $var = 'destination'; + } elseif (!empty($settings['media_picker_field'])) { + $var = 'folder'; + } + + if (empty($var)) { + // Not a media field. + $media = null; + } elseif ($settings['self']) { + // Uses main media. + $media = $this->getMedia(); + } else { + // Uses custom media. + $media = new Media($settings[$var]); + $this->addUpdatedMedia($media); + } + + return $media; + } + /** * @param string $field * @return array|null */ - protected function getFieldSettings(string $field): ?array + public function getFieldSettings(string $field): ?array { if ($field === '') { return null; @@ -88,14 +120,32 @@ trait FlexMediaTrait // Load settings for the field. $schema = $this->getBlueprint()->schema(); $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null; + if (!isset($settings) || !is_array($settings)) { + return null; + } - if (isset($settings['type']) && (in_array($settings['type'], ['avatar', 'file', 'pagemedia']) || !empty($settings['destination']))) { - // Set destination folder. + $type = $settings['type'] ?? ''; + + // Media field. + if (!empty($settings['media_field']) || array_key_exists('destination', $settings) || in_array($type, ['avatar', 'file', 'pagemedia'], true)) { $settings['media_field'] = true; - if (empty($settings['destination']) || in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) { - $settings['destination'] = $this->getMediaFolder(); + $var = 'destination'; + } + + // Media picker field. + if (!empty($settings['media_picker_field']) || in_array($type, ['filepicker', 'pagemediaselect'], true)) { + $settings['media_picker_field'] = true; + $var = 'folder'; + } + + // Set media folder for media fields. + if (isset($var)) { + $folder = $settings[$var] ?? ''; + if (in_array(rtrim($folder, '/'), ['', '@self', 'self@', '@self@'], true)) { + $settings[$var] = $this->getMediaFolder(); $settings['self'] = true; } else { + $settings[$var] = Utils::getPathFromToken($folder, $this); $settings['self'] = false; } } @@ -115,7 +165,6 @@ trait FlexMediaTrait return $settings + ['accept' => '*', 'limit' => 1000, 'self' => true]; } - protected function getMediaFields(): array { // Load settings for the field. @@ -206,12 +255,13 @@ trait FlexMediaTrait */ public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null): void { - $media = $this->getMedia(); + $settings = $this->getMediaFieldSettings($field ?? ''); + + $media = $field ? $this->getMediaField($field) : $this->getMedia(); if (!$media instanceof MediaUploadInterface) { throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads."); } - $settings = $this->getMediaFieldSettings($field ?? ''); $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); $media->copyUploadedFile($uploadedFile, $filename, $settings); $this->clearMediaCache(); @@ -322,13 +372,20 @@ trait FlexMediaTrait foreach ($this->getUpdatedMedia() as $filename => $upload) { if (is_array($upload)) { // Uses new format with [UploadedFileInterface, array]. - $upload = $upload[0]; + $settings = $upload[1]; + if ($settings['destination'] === $media->getPath()) { + $upload = $upload[0]; + } else { + $upload = false; + } } - if ($upload) { - $medium = MediumFactory::fromUploadedFile($upload); + if (false !== $upload) { + $medium = $upload ? MediumFactory::fromUploadedFile($upload) : null; + $updated = true; if ($medium) { - $updated = true; $media->add($filename, $medium); + } else { + $media->hide($filename); } } } @@ -356,7 +413,6 @@ trait FlexMediaTrait return; } - // Upload/delete altered files. /** * @var string $filename diff --git a/user/blueprints/pages/modular/personnes.yaml b/user/blueprints/pages/modular/personnes.yaml index 8345693..80381b8 100644 --- a/user/blueprints/pages/modular/personnes.yaml +++ b/user/blueprints/pages/modular/personnes.yaml @@ -42,4 +42,3 @@ form: .portrait: type: filepicker label: Portrait - folder: 'user/pages/01.home/04._gouvernance' diff --git a/user/blueprints/pages/modular/programmes.yaml b/user/blueprints/pages/modular/programmes.yaml index eab9a2a..94123e4 100644 --- a/user/blueprints/pages/modular/programmes.yaml +++ b/user/blueprints/pages/modular/programmes.yaml @@ -41,4 +41,3 @@ form: name: logo_du_programme type: filepicker label: Logo - folder: 'user/pages/01.home/02._programmes' diff --git a/user/blueprints/pages/modular/rapport_dactivitees.yaml b/user/blueprints/pages/modular/rapport_dactivitees.yaml new file mode 100644 index 0000000..201d4a9 --- /dev/null +++ b/user/blueprints/pages/modular/rapport_dactivitees.yaml @@ -0,0 +1,48 @@ +title: Rapport d'activitées +'@extends': default + +form: + fields: + tabs: + fields: + advanced: + fields: + columns: + fields: + column1: + fields: + name: + default: modular/rapport_dactivitees + '@data-options': '\Grav\Common\Page\Pages::modularTypes' + overrides: + fields: + header.template: + default: modular/rapport_dactivitees + '@data-options': '\Grav\Common\Page\Pages::modularTypes' + rapports: + type: tab + title: Rapports d'activitées + fields: + header.rapports: + name: rapports + type: list + label: Rapports d'activitées + + fields: + .titre_du_rapport: + type: text + label: Titre du rapport + .texte_de_presentation: + type: markdown + label: Texte de présentation du rapport + validate: + type: textarea + .couverture: + name: couv_du_rapport + type: filepicker + label: Image de couverture + .pdf: + type: filepicker + label: pdf à télécharger + accept: + - .pdf diff --git a/user/config/site.yaml b/user/config/site.yaml index 4c564d1..8667c27 100644 --- a/user/config/site.yaml +++ b/user/config/site.yaml @@ -1,7 +1,7 @@ title: 'GIP EPAU' default_lang: fr author: - name: 'Ouidade Soussi' + name: 'Ouidade Soussi Chiadmi' email: ouidade@figureslibres.io taxonomies: - category diff --git a/user/config/system.yaml b/user/config/system.yaml index d63c1dd..8cf59c9 100644 --- a/user/config/system.yaml +++ b/user/config/system.yaml @@ -96,7 +96,7 @@ pages: cache: enabled: true check: - method: file + method: hash driver: auto prefix: g purge_at: '0 4 * * *' diff --git a/user/pages/01.home/02._programmes/logo_coubertin.png b/user/pages/01.home/02._programmes/logo_coubertin.png new file mode 100644 index 0000000..92e9b5b Binary files /dev/null and b/user/pages/01.home/02._programmes/logo_coubertin.png differ diff --git a/user/pages/01.home/02._programmes/programmes.md b/user/pages/01.home/02._programmes/programmes.md index 80e2c3e..f0f6868 100644 --- a/user/pages/01.home/02._programmes/programmes.md +++ b/user/pages/01.home/02._programmes/programmes.md @@ -17,10 +17,10 @@ programmes: - nom_du_programme: Coubertin texte_de_presentation: 'Le programme de recherche-action Coubertin construit le récit, au fil de l’eau, de la conception des ouvrages et des opérations d’aménagement des Jeux Olympiques et Paralympiques de Paris 2024. À travers une observation embarquée au sein de la SOLIDEO, l’équipe de chercheurs analyse la production architecturale et urbaine et sa capacité à transformer les pratiques d’aménagement.' - logo: null + logo: logo_coubertin.png visible: true debugger: true -media_order: 'popsu.png,forum_solution.png,europan_france.jpg' +media_order: 'popsu.png,forum_solution.png,europan_france.jpg,logo_coubertin.png' process: markdown: true twig: false diff --git a/user/pages/01.home/03._rapports-dactivitees/190410_events2event.pdf b/user/pages/01.home/03._rapports-dactivitees/190410_events2event.pdf new file mode 100644 index 0000000..92a948c Binary files /dev/null and b/user/pages/01.home/03._rapports-dactivitees/190410_events2event.pdf differ diff --git a/user/pages/01.home/03._rapports-dactivites/images.jpeg b/user/pages/01.home/03._rapports-dactivitees/images.jpeg similarity index 100% rename from user/pages/01.home/03._rapports-dactivites/images.jpeg rename to user/pages/01.home/03._rapports-dactivitees/images.jpeg diff --git a/user/pages/01.home/03._rapports-dactivitees/rapport_dactivitees.md b/user/pages/01.home/03._rapports-dactivitees/rapport_dactivitees.md new file mode 100644 index 0000000..d5be052 --- /dev/null +++ b/user/pages/01.home/03._rapports-dactivitees/rapport_dactivitees.md @@ -0,0 +1,19 @@ +--- +title: 'Rapports d''activitées' +body_classes: modular +media_order: 'images.jpeg,190410_events2event.pdf' +visible: true +debugger: true +rapports: + - + titre_du_rapport: 'rapport d''activitées POPSU 2019' + texte_de_presentation: "#### # ozMIHEFMJBZNDKCJbnW NX;, \r\noipouepuoaiejhrglnk\r\n_loremnksjdqs _\r\n" + couverture: images.jpeg + pdf: 190410_events2event.pdf +template: modular/rapport_dactivitees +process: + markdown: true + twig: false +--- + +### Rapports d’activitées \ No newline at end of file diff --git a/user/pages/01.home/03._rapports-dactivites/showcase.md b/user/pages/01.home/03._rapports-dactivites/showcase.md deleted file mode 100644 index f71ce81..0000000 --- a/user/pages/01.home/03._rapports-dactivites/showcase.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: 'Rapports d''activités' -body_classes: modular -media_order: images.jpeg -visible: true -debugger: true ---- - -### Rapports d’activités de l’Europe des projets architecturaux et urbains - - -[Lien pour télécharger]() -![]images.jpeg(http://)+ images couv \ No newline at end of file diff --git a/user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md b/user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md index d22def0..31653aa 100644 --- a/user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md +++ b/user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/personnes.md @@ -10,6 +10,7 @@ personnes: portrait: photo_butlen.jpeg debugger: true template: modular/personnes +media_order: photo_butlen.jpeg --- ### Membres du conseil d'administration diff --git a/user/pages/01.home/04._gouvernance/photo_butlen.jpeg b/user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/photo_butlen.jpeg similarity index 100% rename from user/pages/01.home/04._gouvernance/photo_butlen.jpeg rename to user/pages/01.home/04._gouvernance/01._conseil-dadministration/02._membres-du-conseil-dadministration/photo_butlen.jpeg diff --git a/user/pages/01.home/04._gouvernance/Alain Maugard_TROUVE.jpg b/user/pages/01.home/04._gouvernance/02._equipe-epau/Alain Maugard_TROUVE.jpg similarity index 100% rename from user/pages/01.home/04._gouvernance/Alain Maugard_TROUVE.jpg rename to user/pages/01.home/04._gouvernance/02._equipe-epau/Alain Maugard_TROUVE.jpg diff --git a/user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md b/user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md index fd0788b..741cf73 100644 --- a/user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md +++ b/user/pages/01.home/04._gouvernance/02._equipe-epau/personnes.md @@ -12,6 +12,7 @@ personnes: fonction: 'Président de l’Association Europan France' biographie: "Né le 23 avril 1943 à Nérac (Lot-et-Garonne), Alain Maugard est un ancien élève de l’École polytechnique et Ingénieur général des Ponts et Chaussées. Il débute sa carrière au ministère de l’Équipement au service des Affaires économiques et internationales et dans les services urbanisme construction des Directions Départementales. Chef du service de la politique technique à la direction de la Construction et secrétaire permanent du Plan Construction, de 1978 à 1981, il devient successivement directeur adjoint de cabinet des ministres de l’Urbanisme et du Logement Roger Quilliot et Paul Quilès puis directeur de la Construction au ministère de l’Équipement, du Logement, des Transports et de la Mer. En 1990, il est nommé directeur général de l’Établissement public d’aménagement de la Défense (epad), jusqu’en 1993 où il prend la présidence du cstb. Il quitte ces fonctions en 2008 pour rejoindre le Conseil général de l’environnement et du développement durable où il assure la présidence de la section « risques, sécurité, sûreté ». En septembre 2009, il accède à la présidence de Qualibat. \r\nEn outre, Alain Maugard est membre du conseil d’administration de l’Ademe. Il a également piloté le Comité opérationnel (comop) du chantier n°1 « Bâtiments neufs publics et privés » du Grenelle de l’Environnement. \r\n\r\nBibliographie\_:\r\n- Regards sur le bâtiment — Le futur en construction, éditions Le Moniteur, 2006\r\n - Regard sur la ville durable, Alain Maugard et Jean-Pierre Cuisinier, CSTB, mars 2010\r\n - Le BEPOS pour tous (livre électronique), http://outils.xpair.com/livre/bepos-pourtous/7.htm (25 juin 2015) \r\n" portrait: 'Alain Maugard_TROUVE.jpg' +media_order: 'Alain Maugard_TROUVE.jpg' --- ## Équipe EPAU diff --git a/user/pages/01.home/04._gouvernance/1612260288596.jpg b/user/pages/01.home/04._gouvernance/1612260288596.jpg deleted file mode 100644 index 9849e24..0000000 Binary files a/user/pages/01.home/04._gouvernance/1612260288596.jpg and /dev/null differ diff --git a/user/pages/01.home/04._gouvernance/aurelie-cousi-nommee-aux-fonctions-de-directrice.jpg b/user/pages/01.home/04._gouvernance/aurelie-cousi-nommee-aux-fonctions-de-directrice.jpg deleted file mode 100644 index 78464ea..0000000 Binary files a/user/pages/01.home/04._gouvernance/aurelie-cousi-nommee-aux-fonctions-de-directrice.jpg and /dev/null differ diff --git a/user/pages/01.home/04._gouvernance/catherine-chevillot-nouvelle-presidente-de-cite.JPG b/user/pages/01.home/04._gouvernance/catherine-chevillot-nouvelle-presidente-de-cite.JPG deleted file mode 100644 index 92773b4..0000000 Binary files a/user/pages/01.home/04._gouvernance/catherine-chevillot-nouvelle-presidente-de-cite.JPG and /dev/null differ diff --git a/user/pages/01.home/04._gouvernance/gouvernance.md b/user/pages/01.home/04._gouvernance/gouvernance.md index 3976345..e83ce96 100644 --- a/user/pages/01.home/04._gouvernance/gouvernance.md +++ b/user/pages/01.home/04._gouvernance/gouvernance.md @@ -15,7 +15,6 @@ content: admin: children_display_order: collection debugger: true -media_order: '1612260288596.jpg,Alain Maugard_TROUVE.jpg,aurelie-cousi-nommee-aux-fonctions-de-directrice.jpg,catherine-chevillot-nouvelle-presidente-de-cite.JPG,photo_butlen.jpeg' --- # Gouvernance diff --git a/user/pages/01.home/modular.md b/user/pages/01.home/modular.md index 12d3d99..3a4d3f3 100644 --- a/user/pages/01.home/modular.md +++ b/user/pages/01.home/modular.md @@ -11,7 +11,7 @@ content: custom: - _accueil - _programmes - - _rapports-dactivites + - _rapports-dactivitees - _gouvernance - _contact media_order: 'jeux-olypiques-paris-2024-village-athle-lot-e2.jpg,Logotype_GIP_EPAU.svg.png' diff --git a/user/themes/epau-antimatter/templates/modular/personnes.html.twig b/user/themes/epau-antimatter/templates/modular/personnes.html.twig index 044adf7..a0fe46d 100644 --- a/user/themes/epau-antimatter/templates/modular/personnes.html.twig +++ b/user/themes/epau-antimatter/templates/modular/personnes.html.twig @@ -3,12 +3,7 @@
{{ personne.biographie }}
+{{ personne.biographie|markdown }}
{% endif %} {% if personne.portrait %} -{{ programme.texte_de_presentation }}
+{{ programme.texte_de_presentation|markdown }}
{% endif %} {% if programme.logo %} -{{ rapport.texte_de_presentation|markdown }}
+ {% endif %} + {% if rapport.couverture and rapport.pdf %} + +