maj && modif graphique

This commit is contained in:
Tessier
2020-10-30 17:09:07 +01:00
parent 584e63567d
commit b1e14f5f7b
881 changed files with 4606 additions and 7069 deletions
+2
View File
@@ -27,6 +27,8 @@ user/themes/quark/*
!user/themes/quark/.*
user/localhost/config/security.yaml
user/config/security.yaml
user/pages/*
user/pages/.*
# OS Generated
.DS_Store*
+131 -3
View File
@@ -1,3 +1,131 @@
# v1.6.28
## 10/07/2020
1. [](#new)
* Back-ported twig `{% cache %}` tag from Grav 1.7
* Back-ported `Utils::fullPath()` helper function from Grav 1.7
* Back-ported `{{ svg_image() }}` Twig function from Grav 1.7
* Back-ported `Folder::countChildren()` function from Grav 1.7
1. [](#improved)
* Use new `{{ theme_var() }}` enhanced logic from Grav 1.7
* Improved `Excerpts` class with fixes and functionality from Grav 1.7
* Ensure `onBlueprintCreated()` is initialized first
* Do not cache default `404` error page
* Composer update of vendor libraries
* Switched `Caddyfile` to use new Caddy2 syntax + improved usability
1. [](#bugfix)
* Fixed Referer reference during GPM calls.
* Fixed fatal error with toggled lists
# v1.6.27
## 09/01/2020
1. [](#improved)
* Right trim route for safety
* Use the proper ellipsis for summary [#2939](https://github.com/getgrav/grav/pull/2939)
* Left pad schedule times with zeros [#2921](https://github.com/getgrav/grav/pull/2921)
# v1.6.26
## 06/08/2020
1. [](#improved)
* Added new configuration option to control the supported attributes in markdown links [#2882](https://github.com/getgrav/grav/issues/2882)
1. [](#bugfix)
* Fixed blueprint for `system.pages.hide_empty_folders` [#1925](https://github.com/getgrav/grav/issues/2925)
* JSON Route of homepage with no route set is valid
* Fix case-insensitive search of location header [form#425](https://github.com/getgrav/grav-plugin-form/issues/425)
# v1.6.25
## 05/14/2020
1. [](#improved)
* Added system configuration support for `HTTP_X_Forwarded` headers (host disabled by default)
* Updated `PHPUserAgentParser` to 1.0.0
* Bump `Go` to version 1.13 in `travis.yaml`
# v1.6.24
## 04/27/2020
1. [](#improved)
* Added support for `X-Forwarded-Host` [#2891](https://github.com/getgrav/grav/pull/2891)
* Disable XDebug in Travis builds
# v1.6.23
## 03/19/2020
1. [](#new)
* Moved `Parsedown` 1.6 and `ParsedownExtra` 0.7 into `Grav\Framework\Parsedown` to allow fixes
* Added `aliases.php` with references to direct `\Parsedown` and `\ParsedownExtra` references
1. [](#improved)
* Upgraded `jQuery` to latest 3.4.1 version [#2859](https://github.com/getgrav/grav/issues/2859)
1. [](#bugfix)
* Fixed PHP 7.4 issue in ParsedownExtra [#2832](https://github.com/getgrav/grav/issues/2832)
* Fix for [user reported](https://twitter.com/OriginalSicksec) CVE path-based open redirect
* Fix for `stream_set_option` error with PHP 7.4 via Toolbox#28 [#2850](https://github.com/getgrav/grav/issues/2850)
# v1.6.22
## 03/05/2020
1. [](#new)
* Added `Pages::reset()` method
1. [](#improved)
* Updated Negotiation library to address issues [#2513](https://github.com/getgrav/grav/issues/2513)
1. [](#bugfix)
* Fixed issue with search plugins not being able to switch between page translations
* Fixed issues with `Pages::baseRoute()` not picking up active language reliably
* Reverted `validation: strict` fix as it breaks sites, see [#1273](https://github.com/getgrav/grav/issues/1273)
# v1.6.21
## 02/11/2020
1. [](#new)
* Added `ConsoleCommand::setLanguage()` method to set language to be used from CLI
* Added `ConsoleCommand::initializeGrav()` method to properly set up Grav instance to be used from CLI
* Added `ConsoleCommand::initializePlugins()`method to properly set up all plugins to be used from CLI
* Added `ConsoleCommand::initializeThemes()`method to properly set up current theme to be used from CLI
* Added `ConsoleCommand::initializePages()` method to properly set up pages to be used from CLI
1. [](#improved)
* Vendor updates
1. [](#bugfix)
* Fixed `bin/plugin` CLI calling `$themes->init()` way too early (removed it, use above methods instead)
* Fixed call to `$grav['page']` crashing CLI
* Fixed encoding problems when PHP INI setting `default_charset` is not `utf-8` [#2154](https://github.com/getgrav/grav/issues/2154)
# v1.6.20
## 02/03/2020
1. [](#bugfix)
* Fixed incorrect routing caused by `str_replace()` in `Uri::init()` [#2754](https://github.com/getgrav/grav/issues/2754)
* Fixed session cookie is being set twice in the HTTP header [#2745](https://github.com/getgrav/grav/issues/2745)
* Fixed session not restarting if user was invalid (downgrading from Grav 1.7)
* Fixed filesystem iterator calls with non-existing folders
* Fixed `checkbox` field not being saved, requires also Form v4.0.2 [#1225](https://github.com/getgrav/grav/issues/1225)
* Fixed `validation: strict` not working in blueprints [#1273](https://github.com/getgrav/grav/issues/1273)
* Fixed `Data::filter()` removing empty fields (such as empty list) by default [#2805](https://github.com/getgrav/grav/issues/2805)
* Fixed fatal error with non-integer page param value [#2803](https://github.com/getgrav/grav/issues/2803)
* Fixed `Assets::addInlineJs()` parameter type mismatch between v1.5 and v1.6 [#2659](https://github.com/getgrav/grav/issues/2659)
* Fixed `site.metadata` saving issues [#2615](https://github.com/getgrav/grav/issues/2615)
# v1.6.19
## 12/04/2019
1. [](#new)
* Catch PHP 7.4 deprecation messages and report them in debugbar instead of throwing fatal error
1. [](#bugfix)
* Fixed fatal error when calling `{{ grav.undefined }}`
* Fixed multiple issues when there are no pages in the site
* PHP 7.4 fix for [#2750](https://github.com/getgrav/grav/issues/2750)
# v1.6.18
## 12/02/2019
1. [](#bugfix)
* PHP 7.4 fix in `Pages::buildSort()`
* Updated vendor libraries for PHP 7.4 fixes in Twig and other libraries
* Fixed fatal error when `$page->id()` is null [#2731](https://github.com/getgrav/grav/pull/2731)
* Fixed cache conflicts on pages with no set id
* Fix rewrite rule for for `lighttpd` default config [#721](https://github.com/getgrav/grav/pull/2721)
# v1.6.17
## 11/06/2019
@@ -50,7 +178,7 @@
* Support new GRAV_BASEDIR environment variable [#2541](https://github.com/getgrav/grav/pull/2541)
* Allow users to override plugin handler priorities [#2165](https://github.com/getgrav/grav/pull/2165)
1. [](#improved)
* Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)
* Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)
* Updated vendor libraries
* Markdown filter is now page-aware so that it works with modular references [admin#1731](https://github.com/getgrav/grav-plugin-admin/issues/1731)
* Check of `GRAV_USER_INSTANCE` constant is already defined [#2621](https://github.com/getgrav/grav/pull/2621)
@@ -69,7 +197,7 @@
* Fixed `FlexObject::exists()` failing sometimes just after the object has been saved
* Fixed CSV formatter not encoding strings with `"` and `,` properly
* Fixed var order in `Validation.php` [#2610](https://github.com/getgrav/grav/issues/2610)
# v1.6.11
## 06/21/2019
@@ -100,7 +228,7 @@
* Fixed regression with `bin/plugin` not listing the plugins available (1c725c0)
* Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518)
* Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511)
* Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511)
* Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511)
* Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524)
# v1.6.9
+8 -1
View File
@@ -28,6 +28,13 @@ if (!ini_get('date.timezone')) {
date_default_timezone_set('UTC');
}
// Set internal encoding.
if (!\extension_loaded('mbstring')) {
die("'mbstring' extension is not loaded. This is required for Grav to run correctly");
}
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
@@ -55,7 +62,7 @@ $grav->setup($environment);
$grav['config']->init();
$grav['uri']->init();
$grav['users'];
$grav['accounts'];
$app = new Application('Grav Package Manager', GRAV_VERSION);
$app->addCommands(array(
+11 -4
View File
@@ -25,6 +25,17 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
}
if (!ini_get('date.timezone')) {
date_default_timezone_set('UTC');
}
// Set internal encoding.
if (!\extension_loaded('mbstring')) {
die("'mbstring' extension is not loaded. This is required for Grav to run correctly");
}
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
$climate = new League\CLImate\CLImate;
$climate->arguments->add([
'environment' => [
@@ -42,10 +53,6 @@ $environment = $climate->arguments->get('environment');
$grav = Grav::instance(array('loader' => $autoload));
$grav->setup($environment);
if (!ini_get('date.timezone')) {
date_default_timezone_set('UTC');
}
if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
+8 -6
View File
@@ -32,6 +32,13 @@ if (!ini_get('date.timezone')) {
date_default_timezone_set('UTC');
}
// Set internal encoding.
if (!\extension_loaded('mbstring')) {
die("'mbstring' extension is not loaded. This is required for Grav to run correctly");
}
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
@@ -51,12 +58,7 @@ $environment = $climate->arguments->get('environment');
$grav = Grav::instance(array('loader' => $autoload));
$grav->setup($environment);
$grav['config']->init();
$grav['uri']->init();
$grav['users'];
$grav['plugins']->init();
$grav['themes']->init();
$grav->initializeCli();
$app = new Application('Grav Plugins Commands', GRAV_VERSION);
$pattern = '([A-Z]\w+Command\.php)';
+5 -6
View File
@@ -28,8 +28,6 @@
"kodus/psr7-server": "*",
"nyholm/psr7": "^1.0",
"twig/twig": "~1.40",
"erusev/parsedown": "1.6.4",
"erusev/parsedown-extra": "~0.7",
"symfony/yaml": "~4.2.0",
"symfony/console": "~4.2.0",
"symfony/event-dispatcher": "~4.2.0",
@@ -42,9 +40,9 @@
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.0",
"gregwar/image": "2.*",
"donatj/phpuseragentparser": "~0.10",
"donatj/phpuseragentparser": "~1.0",
"pimple/pimple": "~3.2",
"rockettheme/toolbox": "~1.4",
"rockettheme/toolbox": "~1.4.0",
"maximebf/debugbar": "~1.15",
"league/climate": "^3.4",
"antoligy/dom-string-iterators": "^1.0",
@@ -52,7 +50,7 @@
"composer/ca-bundle": "^1.0",
"dragonmantank/cron-expression": "^1.2",
"phive/twig-extensions-deferred": "^1.0",
"willdurand/negotiation": "^2.3"
"willdurand/negotiation": "2.x-dev"
},
"require-dev": {
"codeception/codeception": "^2.4",
@@ -86,7 +84,8 @@
"Grav\\": "system/src/Grav"
},
"files": [
"system/defines.php"
"system/defines.php",
"system/aliases.php"
]
},
"archive": {
Generated
+907 -658
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
chown -R kevin:http .
find . -type f -exec chmod 664 {} \;
find ./bin -type f -exec chmod 775 {} \;
find . -type d -exec chmod 775 {} \;
find . -type d -exec chmod +s {} \;
+10 -9
View File
@@ -20,6 +20,16 @@ if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
}
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());
// Set internal encoding.
if (!\extension_loaded('mbstring')) {
die("'mbstring' extension is not loaded. This is required for Grav to run correctly");
}
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
// Ensure vendor libraries exist
$autoload = __DIR__ . '/vendor/autoload.php';
if (!is_file($autoload)) {
@@ -32,15 +42,6 @@ $loader = require $autoload;
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());
// Set internal encoding if mbstring loaded
if (!\extension_loaded('mbstring')) {
die("'mbstring' extension is not loaded. This is required for Grav to run correctly");
}
mb_internal_encoding('UTF-8');
// Get the Grav instance
$grav = Grav::instance(
array(
+5
View File
@@ -0,0 +1,5 @@
<?php
/** Moved from non-namespaced classes to Grav Framework */
class_alias(Grav\Framework\Parsedown\Parsedown::class, '\Parsedown');
class_alias(Grav\Framework\Parsedown\ParsedownExtra::class, '\ParsedownExtra');
File diff suppressed because one or more lines are too long
+64 -7
View File
@@ -241,13 +241,15 @@ form:
type: commalist
pages.hide_empty_folders:
type: selectize
size: large
label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
classes: fancy
validate:
type: commalist
type: toggle
label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
pages.url_taxonomy_filters:
type: toggle
@@ -513,6 +515,16 @@ form:
validate:
type: bool
pages.markdown.valid_link_attributes:
type: selectize
size: large
label: PLUGIN_ADMIN.VALID_LINK_ATTRIBUTES
help: PLUGIN_ADMIN.VALID_LINK_ATTRIBUTES_HELP
placeholder: "rel, target, id, class, classes"
classes: fancy
validate:
type: commalist
caching:
type: tab
title: PLUGIN_ADMIN.CACHING
@@ -1379,6 +1391,51 @@ form:
label: PLUGIN_ADMIN.CUSTOM_BASE_URL
help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
http_x_forwarded.protocol:
type: toggle
label: HTTP_X_FORWARDED_PROTO Enabled
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
http_x_forwarded.host:
type: toggle
label: HTTP_X_FORWARDED_HOST Enabled
highlight: 0
default: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
http_x_forwarded.port:
type: toggle
label: HTTP_X_FORWARDED_PORT Enabled
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
http_x_forwarded.ip:
type: toggle
label: HTTP_X_FORWARDED IP Enabled
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
accounts.type:
type: hidden
+11
View File
@@ -10,6 +10,11 @@ custom_base_url: '' # Set the base_url manually, e.
username_regex: '^[a-z0-9_-]{3,16}$' # Only lowercase chars, digits, dashes, underscores. 3 - 16 chars
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' # At least one number, one uppercase and lowercase letter, and be at least 8+ chars
intl_enabled: true # Special logic for PHP International Extension (mod_intl)
http_x_forwarded: # Configuration options for the various HTTP_X_FORWARD headers
protocol: true
host: false
port: true
ip: true
languages:
supported: [] # List of languages supported. eg: [en, fr, de]
@@ -54,6 +59,12 @@ pages:
special_chars: # List of special characters to automatically convert to entities
'>': 'gt'
'<': 'lt'
valid_link_attributes: # Valid attributes to pass through via markdown links
- rel
- target
- id
- class
- classes
types: [html,htm,xml,txt,json,rss,atom] # list of valid page types
append_url_extension: '' # Append page's extension in Page urls (e.g. '.html' results in /path/page.html)
expires: 604800 # Page expires time in seconds (604800 seconds = 7 days)
+1 -1
View File
@@ -8,7 +8,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.6.17');
define('GRAV_VERSION', '1.6.28');
define('GRAV_TESTING', false);
define('DS', '/');
+1
View File
@@ -2,4 +2,5 @@
title: Not Found
routable: false
notfound: true
expires: 0
---
@@ -51,7 +51,11 @@ trait LegacyAssetsTrait
// special case to handle old attributes being passed in
if (isset($arguments['attributes'])) {
$old_attributes = $arguments['attributes'];
$arguments = array_merge($arguments, $old_attributes);
if (is_array($old_attributes)) {
$arguments = array_merge($arguments, $old_attributes);
} else {
$arguments['type'] = $old_attributes;
}
}
unset($arguments['attributes']);
+2
View File
@@ -9,6 +9,8 @@
namespace Grav\Common;
use function donatj\UserAgent\parse_user_agent;
/**
* Internally uses the PhpUserAgent package https://github.com/donatj/PhpUserAgent
*/
@@ -136,7 +136,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
{
$messages = $this->checkRequired($data, $rules);
foreach ($data as $key => $field) {
foreach ($data as $key => $child) {
$val = $rules[$key] ?? $rules['*'] ?? null;
$rule = \is_string($val) ? $this->items[$val] : null;
@@ -147,10 +147,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
continue;
}
$messages += Validation::validate($field, $rule);
} elseif (\is_array($field) && \is_array($val)) {
$messages += Validation::validate($child, $rule);
} elseif (\is_array($child) && \is_array($val)) {
// Array has been defined in blueprints.
$messages += $this->validateArray($field, $val);
$messages += $this->validateArray($child, $val);
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
// Undefined/extra item.
throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
@@ -232,8 +232,19 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
continue;
}
if (is_array($value)) {
// Special toggle handling for all the nested data.
$toggle = $toggles[$key] ?? [];
if (!is_array($toggle)) {
if (!$toggle) {
$data[$key] = null;
continue;
}
$toggle = [];
}
// Recursively fetch the items.
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggles[$key] ?? [], $value);
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
} else {
$field = $this->get($value);
// Do not add the field if:
+30 -2
View File
@@ -32,6 +32,12 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
/** @var File */
protected $storage;
/** @var bool */
private $missingValuesAsNull = false;
/** @var bool */
private $keepEmptyValues = true;
/**
* @param array $items
* @param Blueprint|callable $blueprints
@@ -42,6 +48,28 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
$this->blueprints = $blueprints;
}
/**
* @param bool $value
* @return $this
*/
public function setKeepEmptyValues(bool $value)
{
$this->keepEmptyValues = $value;
return $this;
}
/**
* @param bool $value
* @return $this
*/
public function setMissingValuesAsNull(bool $value)
{
$this->missingValuesAsNull = $value;
return $this;
}
/**
* Get value by using dot notation for nested arrays/objects.
*
@@ -202,8 +230,8 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
public function filter()
{
$args = func_get_args();
$missingValuesAsNull = (bool)(array_shift($args) ?: false);
$keepEmptyValues = (bool)(array_shift($args) ?: false);
$missingValuesAsNull = (bool)(array_shift($args) ?? $this->missingValuesAsNull);
$keepEmptyValues = (bool)(array_shift($args) ?? $this->keepEmptyValues);
$this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues);
+10 -1
View File
@@ -166,9 +166,18 @@ class Validation
return (string) $value;
}
/**
* @param mixed $value
* @param array $params
* @param array $field
* @return string|null
*/
protected static function filterCheckbox($value, array $params, array $field)
{
return (bool) $value;
$value = (string)$value;
$field_value = (string)($field['value'] ?? '1');
return $value === $field_value ? $value : null;
}
protected static function filterCommaList($value, array $params, array $field)
+1 -1
View File
@@ -347,7 +347,7 @@ class Debugger
*/
public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
{
if ($errno !== E_USER_DEPRECATED) {
if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) {
if ($this->errorHandler) {
return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
}
+40 -11
View File
@@ -22,6 +22,10 @@ abstract class Folder
*/
public static function lastModifiedFolder($path)
{
if (!file_exists($path)) {
return 0;
}
$last_modified = 0;
/** @var UniformResourceLocator $locator */
@@ -56,6 +60,10 @@ abstract class Folder
*/
public static function lastModifiedFile($path, $extensions = 'md|yaml')
{
if (!file_exists($path)) {
return 0;
}
$last_modified = 0;
/** @var UniformResourceLocator $locator */
@@ -92,21 +100,24 @@ abstract class Folder
*/
public static function hashAllFiles($path)
{
$flags = \RecursiveDirectoryIterator::SKIP_DOTS;
$files = [];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new \RecursiveDirectoryIterator($path, $flags);
}
if (file_exists($path)) {
$flags = \RecursiveDirectoryIterator::SKIP_DOTS;
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new \RecursiveDirectoryIterator($path, $flags);
}
foreach ($iterator as $file) {
$files[] = $file->getPathname() . '?'. $file->getMTime();
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
$files[] = $file->getPathname() . '?'. $file->getMTime();
}
}
return md5(serialize($files));
@@ -199,6 +210,9 @@ abstract class Folder
if ($path === false) {
throw new \RuntimeException("Path doesn't exist.");
}
if (!file_exists($path)) {
return [];
}
$compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
$pattern = $params['pattern'] ?? null;
@@ -494,4 +508,19 @@ abstract class Folder
return $include_target ? @rmdir($folder) : true;
}
/**
* Does a directory contain children
*
* @param string $directory
* @return int|false
*/
public static function countChildren($directory) {
if (!is_dir($directory)) {
return false;
}
$directories = glob($directory . '/*', GLOB_ONLYDIR);
return count($directories);
}
}
+2 -1
View File
@@ -11,6 +11,7 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\Data\Data;
use Grav\Common\GPM\Common\Package as BasePackage;
use Grav\Framework\Parsedown\Parsedown;
class Package extends BasePackage
{
@@ -23,7 +24,7 @@ class Package extends BasePackage
$this->settings = $package->toArray();
$html_description = \Parsedown::instance()->line($this->__get('description'));
$html_description = Parsedown::instance()->line($this->__get('description'));
$this->data->set('slug', $package->__get('slug'));
$this->data->set('description_html', $html_description);
$this->data->set('description_plain', strip_tags($html_description));
+4 -2
View File
@@ -36,7 +36,6 @@ class Response
private static $defaults = [
'curl' => [
CURLOPT_REFERER => 'Grav GPM',
CURLOPT_USERAGENT => 'Grav GPM',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
@@ -285,6 +284,7 @@ class Response
$options['fopen']['notification'] = ['self', 'progress'];
}
$options['fopen']['header'] = 'Referer: ' . Grav::instance()['uri']->rootUrl(true);
if (isset($options['fopen']['ssl'])) {
$ssl = $options['fopen']['ssl'];
unset($options['fopen']['ssl']);
@@ -367,6 +367,8 @@ class Response
*/
private static function curlExecFollow($ch, $options, $callback)
{
curl_setopt_array($ch, [ CURLOPT_REFERER => Grav::instance()['uri']->rootUrl(true) ]);
if ($callback) {
curl_setopt_array(
$ch,
@@ -409,7 +411,7 @@ class Response
} else {
$code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
if ($code === 301 || $code === 302 || $code === 303) {
preg_match('/Location:(.*?)\n/', $header, $matches);
preg_match('/(?:^|\n)Location:(.*?)\n/i', $header, $matches);
$uri = trim(array_pop($matches));
} else {
$code = 0;
+35 -4
View File
@@ -170,6 +170,29 @@ class Grav extends Container
return $this;
}
/**
* Initialize CLI environment.
*
* Call after `$grav->setup($environment)`
*
* - Load configuration
* - Disable debugger
* - Set timezone, locale
* - Load plugins
* - Set Users type to be used in the site
*
* This method WILL NOT initialize assets, twig or pages.
*
* @param string|null $environment
* @return $this
*/
public function initializeCli()
{
InitializeProcessor::initializeCli($this);
return $this;
}
/**
* Process a request
*/
@@ -238,7 +261,7 @@ class Grav extends Container
);
$default = function (ServerRequestInterface $request) {
return new Response(404);
return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-cache, no-store, must-revalidate'], 'Not Found');
};
/** @var Debugger $debugger */
@@ -293,7 +316,10 @@ class Grav extends Container
/** @var Uri $uri */
$uri = $this['uri'];
//Check for code in route
// Clean route for redirect
$route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
// Check for code in route
$regex = '/.*(\[(30[1-7])\])$/';
preg_match($regex, $route, $matches);
if ($matches) {
@@ -439,11 +465,16 @@ class Grav extends Container
* Used to call closures.
*
* Source: http://stackoverflow.com/questions/419804/closures-as-class-members
*
* @param string $method
* @param array $args
* @return
*/
public function __call($method, $args)
{
$closure = $this->{$method};
\call_user_func_array($closure, $args);
$closure = $this->{$method} ?? null;
return is_callable($closure) ? $closure(...$args) : null;
}
/**
+43 -10
View File
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Helpers
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -32,7 +32,7 @@ class Excerpts
$excerpt = static::processLinkExcerpt($excerpt, $page, 'image');
$excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href'];
unset ($excerpt['element']['attributes']['href']);
unset($excerpt['element']['attributes']['href']);
$excerpt = static::processImageExcerpt($excerpt, $page);
@@ -43,6 +43,26 @@ class Excerpts
return $html;
}
/**
* Process Grav page link URL from HTML tag
*
* @param string $html HTML tag e.g. `<a href="../foo">Page Link</a>`
* @param PageInterface|null $page Page, defaults to the current page object
* @return string Returns final HTML string
*/
public static function processLinkHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'a');
$original_href = $excerpt['element']['attributes']['href'];
$excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
$excerpt['element']['attributes']['data-href'] = $original_href;
$html = static::getHtmlFromExcerpt($excerpt);
return $html;
}
/**
* Get an Excerpt array from a chunk of HTML
*
@@ -52,22 +72,35 @@ class Excerpts
*/
public static function getExcerptFromHtml($html, $tag)
{
$doc = new \DOMDocument();
$doc->loadHTML($html);
$images = $doc->getElementsByTagName($tag);
$excerpt = null;
$doc = new \DOMDocument('1.0', 'UTF-8');
$internalErrors = libxml_use_internal_errors(true);
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_use_internal_errors($internalErrors);
foreach ($images as $image) {
$elements = $doc->getElementsByTagName($tag);
$excerpt = null;
$inner = [];
/** @var \DOMElement $element */
foreach ($elements as $element) {
$attributes = [];
foreach ($image->attributes as $name => $value) {
foreach ($element->attributes as $name => $value) {
$attributes[$name] = $value->value;
}
$excerpt = [
'element' => [
'name' => $image->tagName,
'name' => $element->tagName,
'attributes' => $attributes
]
];
foreach ($element->childNodes as $node) {
$inner[] = $doc->saveHTML($node);
}
$excerpt = array_merge_recursive($excerpt, ['element' => ['text' => implode('', $inner)]]);
}
return $excerpt;
@@ -95,7 +128,7 @@ class Excerpts
if (isset($element['text'])) {
$html .= '>';
$html .= $element['text'];
$html .= is_array($element['text']) ? static::getHtmlFromExcerpt(['element' => $element['text']]) : $element['text'];
$html .= '</'.$element['name'].'>';
} else {
$html .= ' />';
@@ -11,8 +11,9 @@ namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Framework\Parsedown\Parsedown as ParsedownLib;
class Parsedown extends \Parsedown
class Parsedown extends ParsedownLib
{
use ParsedownGravTrait;
@@ -11,8 +11,9 @@ namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Framework\Parsedown\ParsedownExtra as ParsedownExtraLib;
class ParsedownExtra extends \ParsedownExtra
class ParsedownExtra extends ParsedownExtraLib
{
use ParsedownGravTrait;
@@ -87,7 +87,7 @@ class Excerpts
);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
$valid_attributes = Grav::instance()['config']->get('system.pages.markdown.valid_link_attributes');
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
@@ -232,6 +232,7 @@ class Excerpts
$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(
@@ -516,6 +516,15 @@ class ImageMedium extends Medium
return $this;
}
/**
* Handle this commonly used variant
*/
public function cropZoom()
{
$this->__call('zoomCrop', func_get_args());
return $this;
}
/**
* Forward the call to the image processing method.
*
@@ -525,10 +534,6 @@ class ImageMedium extends Medium
*/
public function __call($method, $args)
{
if ($method === 'cropZoom') {
$method = 'zoomCrop';
}
if (!\in_array($method, self::$magic_actions, true)) {
return parent::__call($method, $args);
}
+11 -7
View File
@@ -608,12 +608,12 @@ class Page implements PageInterface
return $content;
}
return mb_strimwidth($content, 0, $size, '...', 'utf-8');
return mb_strimwidth($content, 0, $size, '', 'UTF-8');
}
$summary = Utils::truncateHtml($content, $size);
return html_entity_decode($summary);
return html_entity_decode($summary, ENT_COMPAT | ENT_HTML401, 'UTF-8');
}
/**
@@ -659,7 +659,7 @@ class Page implements PageInterface
// Load cached content
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
$cache_id = md5('page' . $this->id());
$cache_id = md5('page' . $this->getCacheKey());
$content_obj = $cache->fetch($cache_id);
if (is_array($content_obj)) {
@@ -865,7 +865,7 @@ class Page implements PageInterface
public function cachePageContent()
{
$cache = Grav::instance()['cache'];
$cache_id = md5('page' . $this->id());
$cache_id = md5('page' . $this->getCacheKey());
$cache->save($cache_id, ['content' => $this->content, 'content_meta' => $this->content_meta]);
}
@@ -1200,7 +1200,7 @@ class Page implements PageInterface
/**
* @return string
*/
protected function getCacheKey(): string
public function getCacheKey(): string
{
return $this->id();
}
@@ -1694,7 +1694,7 @@ class Page implements PageInterface
$metadata['generator'] = 'GravCMS';
// Get initial metadata for the page
$metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata'));
$metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata', []));
if (isset($this->header->metadata) && is_array($this->header->metadata)) {
// Merge any site.metadata settings in with page metadata
@@ -2009,6 +2009,10 @@ class Page implements PageInterface
*/
public function id($var = null)
{
if (null === $this->id) {
// We need to set unique id to avoid potential cache conflicts between pages.
$var = time() . md5($this->filePath());
}
if ($var !== null) {
// store unique per language
$active_lang = Grav::instance()['language']->getLanguage() ?: '';
@@ -2824,7 +2828,7 @@ class Page implements PageInterface
if ($pagination) {
$params = $collection->params();
$limit = $params['limit'] ?? 0;
$limit = (int)($params['limit'] ?? 0);
$start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0;
if ($limit && $collection->count() > $limit) {
+43 -23
View File
@@ -95,6 +95,8 @@ class Pages
protected $initialized = false;
protected $active_lang;
/**
* @var Types
*/
@@ -143,7 +145,7 @@ class Pages
*/
public function baseRoute($lang = null)
{
$key = $lang ?: 'default';
$key = $lang ?: $this->active_lang ?: 'default';
if (!isset($this->baseRoute[$key])) {
/** @var Language $language */
@@ -236,6 +238,16 @@ class Pages
$this->check_method = strtolower($method);
}
/**
* Reset pages (used in search indexing etc).
*/
public function reset()
{
$this->initialized = false;
$this->init();
}
/**
* Class initialization. Must be called before using this class.
*/
@@ -598,8 +610,8 @@ class Pages
}
if (empty($blueprint->initialized)) {
$this->grav->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $type]));
$blueprint->initialized = true;
$this->grav->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $type]));
}
return $blueprint;
@@ -958,6 +970,9 @@ class Pages
$pages_dir = $locator->findResource('page://');
// Set active language
$this->active_lang = $language->getActive();
if ($config->get('system.cache.enabled')) {
/** @var Cache $cache */
$cache = $this->grav['cache'];
@@ -982,17 +997,19 @@ class Pages
$this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($this->pages_cache_id);
if (!$this->instances) {
$cached = $cache->fetch($this->pages_cache_id);
if ($cached) {
$this->grav['debugger']->addMessage('Page cache hit.');
list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cached;
// If pages was found in cache, set the taxonomy
$taxonomy->taxonomy($taxonomy_map);
} else {
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
// recurse pages and cache result
$this->resetPages($pages_dir);
} else {
// If pages was found in cache, set the taxonomy
$this->grav['debugger']->addMessage('Page cache hit.');
$taxonomy->taxonomy($taxonomy_map);
}
} else {
$this->recurse($pages_dir);
@@ -1261,14 +1278,13 @@ class Pages
{
$list = [];
$header_default = null;
$header_query = null;
$header_query = [];
// do this header query work only once
if (strpos($order_by, 'header.') === 0) {
$header_query = explode('|', str_replace('header.', '', $order_by));
if (isset($header_query[1])) {
$header_default = $header_query[1];
}
$query = explode('|', str_replace('header.', '', $order_by), 2);
$header_query = array_shift($query) ?? '';
$header_default = array_shift($query);
}
foreach ($pages as $key => $info) {
@@ -1306,11 +1322,17 @@ class Pages
case 'folder':
$list[$key] = $child->folder();
break;
case (is_string($header_query[0])):
$child_header = new Header((array)$child->header());
$header_value = $child_header->get($header_query[0]);
case 'manual':
case 'default':
default:
if (is_string($header_query)) {
$child_header = $child->header();
if (!$child_header instanceof Header) {
$child_header = new Header((array)$child_header);
}
$header_value = $child_header->get($header_query);
if (is_array($header_value)) {
$list[$key] = implode(',',$header_value);
$list[$key] = implode(',', $header_value);
} elseif ($header_value) {
$list[$key] = $header_value;
} else {
@@ -1318,11 +1340,9 @@ class Pages
}
$sort_flags = $sort_flags ?: SORT_REGULAR;
break;
case 'manual':
case 'default':
default:
$list[$key] = $key;
$sort_flags = $sort_flags ?: SORT_REGULAR;
}
$list[$key] = $key;
$sort_flags = $sort_flags ?: SORT_REGULAR;
}
}
+1 -1
View File
@@ -305,7 +305,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
// Create new config object and set it on the page object so it's cached for next time
$page->modifyHeader($class_name_merged, new Data($header));
} else if (isset($page_header->{$class_name_merged})) {
} elseif (isset($page_header->{$class_name_merged})) {
$merged = $page_header->{$class_name_merged};
$header = $merged->toArray();
}
@@ -10,6 +10,7 @@
namespace Grav\Common\Processors;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Session\Exceptions\SessionException;
@@ -22,6 +23,22 @@ class InitializeProcessor extends ProcessorBase
public $id = 'init';
public $title = 'Initialize';
/** @var bool */
private static $cli_initialized = false;
/**
* @param Grav $grav
*/
public static function initializeCli(Grav $grav)
{
if (!static::$cli_initialized) {
static::$cli_initialized = true;
$instance = new static($grav);
$instance->processCli();
}
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
@@ -77,4 +94,35 @@ class InitializeProcessor extends ProcessorBase
return $handler->handle($request);
}
public function processCli(): void
{
// Load configuration.
$this->container['config']->init();
$this->container['plugins']->setup();
// Disable debugger.
$this->container['debugger']->enabled(false);
// Set timezone, locale.
/** @var Config $config */
$config = $this->container['config'];
$timezone = $config->get('system.timezone');
if ($timezone) {
date_default_timezone_set($timezone);
}
$this->container->setLocale();
// Load plugins.
$this->container['plugins']->init();
// Initialize URI.
/** @var Uri $uri */
$uri = $this->container['uri'];
$uri->init();
// Load accounts.
// TODO: remove in 2.0.
$this->container['accounts'];
}
}
+2 -2
View File
@@ -69,7 +69,7 @@ class Cron
'name_year' => 'année',
'text_period' => 'Chaque %s',
'text_mins' => 'à %s minutes',
'text_time' => 'à %s:%s',
'text_time' => 'à %02s:%02s',
'text_dow' => 'le %s',
'text_month' => 'de %s',
'text_dom' => 'le %s',
@@ -86,7 +86,7 @@ class Cron
'name_year' => 'year',
'text_period' => 'Every %s',
'text_mins' => 'at %s minutes past the hour',
'text_time' => 'at %s:%s',
'text_time' => 'at %02s:%02s',
'text_dow' => 'on %s',
'text_month' => 'of %s',
'text_dom' => 'on the %s',
@@ -26,6 +26,19 @@ class PagesServiceProvider implements ServiceProviderInterface
return new Pages($c);
};
if (\defined('GRAV_CLI')) {
$container['page'] = static function ($c) {
$path = $c['locator']->findResource('system://pages/notfound.md');
$page = new Page();
$page->init(new \SplFileInfo($path));
$page->routable(false);
return $page;
};
return;
}
$container['page'] = function ($c) {
/** @var Grav $c */
@@ -0,0 +1,56 @@
<?php
/**
* @package Grav\Common\Twig
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\Node;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
class TwigNodeCache extends Node
{
/**
* @param string $key unique name for key
* @param int $lifetime in seconds
* @param Node $body
* @param integer $lineno
* @param string $tag
*/
public function __construct(string $key, int $lifetime, Node $body, $lineno, $tag = null)
{
parent::__construct(array('body' => $body), array( 'key' => $key, 'lifetime' => $lifetime), $lineno, $tag);
}
/**
* {@inheritDoc}
*/
public function compile(Compiler $compiler)
{
$boo = $this->getAttribute('key');
$compiler
->addDebugInfo($this)
->write("\$cache = \\Grav\\Common\\Grav::instance()['cache'];\n")
->write("\$key = \"twigcache-\" . \"" . $this->getAttribute('key') . "\";\n")
->write("\$lifetime = " . $this->getAttribute('lifetime') . ";\n")
->write("\$cache_body = \$cache->fetch(\$key);\n")
->write("if (\$cache_body === false) {\n")
->indent()
->write("ob_start();\n")
->indent()
->subcompile($this->getNode('body'))
->outdent()
->write("\n")
->write("\$cache_body = ob_get_clean();\n")
->write("\$cache->save(\$key, \$cache_body, \$lifetime);\n")
->outdent()
->write("}\n")
->write("echo \$cache_body;\n")
;
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @package Grav\Common\Twig
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\TokenParser;
use Grav\Common\Grav;
use Grav\Common\Twig\Node\TwigNodeCache;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Adds ability to cache Twig between tags.
*
* {% cache 600 %}
* {{ some_complex_work() }}
* {% endcache %}
*
* Where the `600` is an optional lifetime in seconds
*/
class TwigTokenParserCache extends AbstractTokenParser
{
/**
* {@inheritDoc}
*/
public function parse(Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$key = $this->parser->getVarName() . $lineno;
$lifetime = Grav::instance()['cache']->getLifetime();
// Check for optional lifetime override
if (!$stream->test(Token::BLOCK_END_TYPE)) {
$lifetime_expr = $this->parser->getExpressionParser()->parseExpression();
$lifetime = $lifetime_expr->getAttribute('value');
}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideCacheEnd'), true);
$stream->expect(Token::BLOCK_END_TYPE);
return new TwigNodeCache($key, $lifetime, $body, $lineno, $this->getTag());
}
/**
* Decide if current token marks end of cache block.
*
* @param Token $token
* @return bool
*/
public function decideCacheEnd(Token $token)
{
return $token->test('endcache');
}
/**
* {@inheritDoc}
*/
public function getTag()
{
return 'cache';
}
}
+98 -44
View File
@@ -11,10 +11,12 @@ namespace Grav\Common\Twig;
use Cron\CronExpression;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Collection;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Media;
use Grav\Common\Scheduler\Cron;
use Grav\Common\Security;
@@ -25,6 +27,7 @@ use Grav\Common\Twig\TokenParser\TwigTokenParserSwitch;
use Grav\Common\Twig\TokenParser\TwigTokenParserThrow;
use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch;
use Grav\Common\Twig\TokenParser\TwigTokenParserMarkdown;
use Grav\Common\Twig\TokenParser\TwigTokenParserCache;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Common\Yaml;
@@ -167,13 +170,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFunction('exif', [$this, 'exifFunc']),
new \Twig_SimpleFunction('media_directory', [$this, 'mediaDirFunc']),
new \Twig_SimpleFunction('body_class', [$this, 'bodyClassFunc']),
new \Twig_SimpleFunction('theme_var', [$this, 'themeVarFunc']),
new \Twig_SimpleFunction('header_var', [$this, 'pageHeaderVarFunc']),
new \Twig_SimpleFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]),
new \Twig_SimpleFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]),
new \Twig_SimpleFunction('read_file', [$this, 'readFileFunc']),
new \Twig_SimpleFunction('nicenumber', [$this, 'niceNumberFunc']),
new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFunc']),
new \Twig_SimpleFunction('cron', [$this, 'cronFunc']),
new \Twig_SimpleFunction('svg_image', [$this, 'svgImageFunction']),
new \Twig_SimpleFunction('xss', [$this, 'xssFunc']),
@@ -201,6 +205,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new TwigTokenParserStyle(),
new TwigTokenParserMarkdown(),
new TwigTokenParserSwitch(),
new TwigTokenParserCache(),
];
}
@@ -1055,7 +1060,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/
public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0)
{
return json_decode(html_entity_decode($str), $assoc, $depth, $options);
return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options);
}
/**
@@ -1281,17 +1286,64 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/**
* Get a theme variable
* Will try to get the variable for the current page, if not found, it tries it's parent page on up to root.
* If still not found, will use the theme's configuration value,
* If still not found, will use the $default value passed in
*
* @param string $var
* @param bool $default
* @param $context Twig Context
* @param string $var variable to be found (using dot notation)
* @param null $default the default value to be used as last resort
* @param null $page an optional page to use for the current page
* @param bool $exists toggle to simply return the page where the variable is set, else null
* @return string
*/
public function themeVarFunc($var, $default = null)
public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false)
{
$header = $this->grav['page']->header();
$header_classes = $header->{$var} ?? null;
$page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null;
return $header_classes ?: $this->config->get('theme.' . $var, $default);
// Try to find var in the page headers
if ($page instanceof PageInterface && $page->exists()) {
// Loop over pages and look for header vars
while ($page && !$page->root()) {
$header = new Data((array)$page->header());
$value = $header->get($var);
if (isset($value)) {
if ($exists) {
return $page;
} else {
return $value;
}
}
$page = $page->parent();
}
}
if ($exists) {
return false;
} else {
return Grav::instance()['config']->get('theme.' . $var, $default);
}
}
/**
* Look for a page header variable in an array of pages working its way through until a value is found
*
* @param $context
* @param string $var the variable to look for in the page header
* @param string|string[]|null $pages array of pages to check (current page upwards if not null)
* @param bool $exists if true, return the page where the var is found, not the value
* @return mixed
* @deprecated 1.7 Use themeVarFunc() instead
*/
public function pageHeaderVarFunc($context, $var, $pages = null)
{
if (is_array($pages)) {
$page = array_shift($pages);
} else {
$page = null;
}
return $this->themeVarFunc($context, $var, null, $page);
}
/**
@@ -1319,41 +1371,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return $body_classes;
}
/**
* Look for a page header variable in an array of pages working its way through until a value is found
*
* @param string $var
* @param string|string[]|null $pages
* @return mixed
*/
public function pageHeaderVarFunc($var, $pages = null)
{
if ($pages === null) {
$pages = $this->grav['page'];
}
// Make sure pages are an array
if (!\is_array($pages)) {
$pages = [$pages];
}
// Loop over pages and look for header vars
foreach ($pages as $page) {
if (\is_string($page)) {
$page = $this->grav['pages']->find($page);
}
if ($page) {
$header = $page->header();
if (isset($header->{$var})) {
return $header->{$var};
}
}
}
return null;
}
/**
* Dump/Encode data into YAML format
*
@@ -1442,4 +1459,41 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
break;
}
}
/**
* Returns the content of an SVG image and adds extra classes as needed
*
* @param $path
* @param $classes
* @return string|string[]|null
*/
public static function svgImageFunction($path, $classes)
{
$path = Utils::fullPath($path);
if (file_exists($path)) {
$svg = file_get_contents($path);
$classes = " inline-block $classes";
$matched = false;
//Look for existing class
$svg = preg_replace_callback('/^<svg.*?(class=\"(.*?)").*>/', function($matches) use ($classes, &$matched) {
if (isset($matches[2])) {
$new_classes = $matches[2] . $classes;
$matched = true;
return str_replace($matches[1], "class=\"$new_classes\"", $matches[0]);
}
}, $svg
);
// no matches found just add the class
if (!$matched) {
$classes = trim($classes);
$svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
}
return $svg;
}
}
}
+22 -17
View File
@@ -151,7 +151,7 @@ class Uri
$this->url = $this->base . $this->uri;
$uri = str_replace(static::filterPath($this->root), '', $this->url);
$uri = Utils::replaceFirstOccurrence(static::filterPath($this->root), '', $this->url);
// remove the setup.php based base if set:
$setup_base = $grav['pages']->base();
@@ -195,7 +195,7 @@ class Uri
// set the new url
$this->url = $this->root . $path;
$this->path = static::cleanPath($path);
$this->content_path = trim(str_replace($this->base, '', $this->path), '/');
$this->content_path = trim(Utils::replaceFirstOccurrence($this->base, '', $this->path), '/');
if ($this->content_path !== '') {
$this->paths = explode('/', $this->content_path);
}
@@ -306,7 +306,7 @@ class Uri
public function param($id)
{
if (isset($this->params[$id])) {
return html_entity_decode(rawurldecode($this->params[$id]));
return html_entity_decode(rawurldecode($this->params[$id]), ENT_COMPAT | ENT_HTML401, 'UTF-8');
}
return false;
@@ -340,7 +340,7 @@ class Uri
return $this->url;
}
$url = str_replace($this->base, '', rtrim($this->url, '/'));
$url = Utils::replaceFirstOccurrence($this->base, '', rtrim($this->url, '/'));
return $url ?: '/';
}
@@ -489,7 +489,7 @@ class Uri
return $this->uri;
}
return str_replace($this->root_path, '', $this->uri);
return Utils::replaceFirstOccurrence($this->root_path, '', $this->uri);
}
/**
@@ -531,7 +531,7 @@ class Uri
return $this->root;
}
return str_replace($this->base, '', $this->root);
return Utils::replaceFirstOccurrence($this->base, '', $this->root);
}
/**
@@ -541,7 +541,9 @@ class Uri
*/
public function currentPage()
{
return $this->params['page'] ?? 1;
$page = (int)($this->params['page'] ?? 1);
return max(1, $page);
}
/**
@@ -629,9 +631,9 @@ class Uri
{
if (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif (getenv('HTTP_X_FORWARDED_FOR')) {
} elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
} elseif (getenv('HTTP_X_FORWARDED')) {
} elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED');
} elseif (getenv('HTTP_FORWARDED_FOR')) {
$ip = getenv('HTTP_FORWARDED_FOR');
@@ -783,7 +785,7 @@ class Uri
}
// special check to see if path checking is required.
$just_path = str_replace($normalized_url, '', $normalized_path);
$just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path);
if ($normalized_url === '/' || $just_path === $page->path()) {
$url_path = $normalized_url;
} else {
@@ -852,7 +854,7 @@ class Uri
}
// strip base from this path
$target_path = str_replace($uri->rootUrl(), '', $target_path);
$target_path = Utils::replaceFirstOccurrence($uri->rootUrl(), '', $target_path);
// set to / if root
if (empty($target_path)) {
@@ -877,7 +879,7 @@ class Uri
// Handle route only
if ($route_only) {
$url_path = str_replace(static::filterPath($base_url), '', $url_path);
$url_path = Utils::replaceFirstOccurrence(static::filterPath($base_url), '', $url_path);
}
// transform back to string/array as needed
@@ -998,7 +1000,7 @@ class Uri
}
// special check to see if path checking is required.
$just_path = str_replace($normalized_url, '', $normalized_path);
$just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path);
if ($just_path === $page->path()) {
return $normalized_url;
}
@@ -1148,7 +1150,7 @@ class Uri
protected function createFromEnvironment(array $env)
{
// Build scheme.
if (isset($env['HTTP_X_FORWARDED_PROTO'])) {
if (isset($env['HTTP_X_FORWARDED_PROTO']) && Grav::instance()['config']->get('system.http_x_forwarded.protocol')) {
$this->scheme = $env['HTTP_X_FORWARDED_PROTO'];
} elseif (isset($env['X-FORWARDED-PROTO'])) {
$this->scheme = $env['X-FORWARDED-PROTO'];
@@ -1166,11 +1168,14 @@ class Uri
$this->password = $env['PHP_AUTH_PW'] ?? null;
// Build host.
$hostname = 'localhost';
if (isset($env['HTTP_HOST'])) {
if (isset($env['HTTP_X_FORWARDED_HOST']) && Grav::instance()['config']->get('system.http_x_forwarded.host')) {
$hostname = $env['HTTP_X_FORWARDED_HOST'];
} else if (isset($env['HTTP_HOST'])) {
$hostname = $env['HTTP_HOST'];
} elseif (isset($env['SERVER_NAME'])) {
$hostname = $env['SERVER_NAME'];
} else {
$hostname = 'localhost';
}
// Remove port from HTTP_HOST generated $hostname
$hostname = Utils::substrToString($hostname, ':');
@@ -1178,7 +1183,7 @@ class Uri
$this->host = $this->validateHostname($hostname) ? $hostname : 'unknown';
// Build port.
if (isset($env['HTTP_X_FORWARDED_PORT'])) {
if (isset($env['HTTP_X_FORWARDED_PORT']) && Grav::instance()['config']->get('system.http_x_forwarded.port')) {
$this->port = (int)$env['HTTP_X_FORWARDED_PORT'];
} elseif (isset($env['X-FORWARDED-PORT'])) {
$this->port = (int)$env['X-FORWARDED-PORT'];
+22
View File
@@ -129,6 +129,28 @@ abstract class Utils
return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
}
/**
* Helper method to find the full path to a file, be it a stream, a relative path, or
* already a full path
*
* @param $path
* @return string
*/
public static function fullPath($path)
{
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$path = $locator->findResource($path, true);
} elseif (!Utils::startsWith($path, GRAV_ROOT)) {
$base_url = Grav::instance()['base_url'];
$path = GRAV_ROOT . '/' . ltrim(Utils::replaceFirstOccurrence($base_url, '', $path), '/');
}
return $path;
}
/**
* Check if the $haystack string starts with the substring $needle
*
+140 -1
View File
@@ -10,6 +10,10 @@
namespace Grav\Console;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Grav\Common\Processors\InitializeProcessor;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -18,6 +22,13 @@ class ConsoleCommand extends Command
{
use ConsoleTrait;
/** @var bool */
private $plugins_initialized = false;
/** @var bool */
private $themes_initialized = false;
/** @var bool */
private $pages_initialized = false;
/**
* @param InputInterface $input
* @param OutputInterface $output
@@ -31,12 +42,140 @@ class ConsoleCommand extends Command
}
/**
*
* Override with your implementation.
*/
protected function serve()
{
}
/**
* Initialize Grav.
*
* - Load configuration
* - Disable debugger
* - Set timezone, locale
* - Load plugins
* - Set Users type to be used in the site
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeGrav()
{
InitializeProcessor::initializeCli(Grav::instance());
return $this;
}
/**
* Set language to be used in CLI.
*
* @param string|null $code
*/
final protected function setLanguage(string $code = null)
{
$this->initializeGrav();
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
if ($language->enabled()) {
if ($code && $language->validate($code)) {
$language->setActive($code);
} else {
$language->setActive($language->getDefault());
}
}
}
/**
* Properly initialize plugins.
*
* - call $this->initializeGrav()
* - call onPluginsInitialized event
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePlugins()
{
if (!$this->plugins_initialized) {
$this->plugins_initialized = true;
$this->initializeGrav();
// Initialize plugins.
$grav = Grav::instance();
$grav->fireEvent('onPluginsInitialized');
}
return $this;
}
/**
* Properly initialize themes.
*
* - call $this->initializePlugins()
* - initialize theme (call onThemeInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeThemes()
{
if (!$this->themes_initialized) {
$this->themes_initialized = true;
$this->initializePlugins();
// Initialize themes.
$grav = Grav::instance();
$grav['themes']->init();
}
return $this;
}
/**
* Properly initialize pages.
*
* - call $this->initializeThemes()
* - initialize assets (call onAssetsInitialized event)
* - initialize twig (calls the twig events)
* - initialize pages (calls onPagesInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePages()
{
if (!$this->pages_initialized) {
$this->pages_initialized = true;
$this->initializeThemes();
$grav = Grav::instance();
// Initialize assets.
$grav['assets']->init();
$grav->fireEvent('onAssetsInitialized');
// Initialize twig.
$grav['twig']->init();
// Initialize pages.
$pages = $grav['pages'];
$pages->init();
$grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages]));
}
return $this;
}
protected function displayGPMRelease()
{
$this->output->writeln('');
@@ -177,7 +177,7 @@ class SelfupgradeCommand extends ConsoleCommand
}
/**
* @param Package $package
* @param array $package
*
* @return string
*/
@@ -92,7 +92,7 @@ class CsvFormatter extends AbstractFormatter
if ($null_replace) {
array_walk($csv_line, function(&$el) use ($null_replace) {
$el = str_replace($null_replace, null, $el);
$el = str_replace($null_replace, "\0", $el);
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,532 @@
<?php
/**
* @package Grav\Framework\Parsedown
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Parsedown;
/*
* Parsedown Extra
* http://parsedown.org
*
* (c) Emanuil Rusev
* http://erusev.com
*
* This file ported from officiall ParsedownExtra repo and kept for compatibility.
*/
class ParsedownExtra extends Parsedown
{
# ~
const version = '0.7.0';
# ~
function __construct()
{
if (parent::version < '1.5.0')
{
throw new Exception('ParsedownExtra requires a later version of Parsedown');
}
$this->BlockTypes[':'] []= 'DefinitionList';
$this->BlockTypes['*'] []= 'Abbreviation';
# identify footnote definitions before reference definitions
array_unshift($this->BlockTypes['['], 'Footnote');
# identify footnote markers before before links
array_unshift($this->InlineTypes['['], 'FootnoteMarker');
}
#
# ~
function text($text)
{
$markup = parent::text($text);
# merge consecutive dl elements
$markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
# add footnotes
if (isset($this->DefinitionData['Footnote']))
{
$Element = $this->buildFootnoteElement();
$markup .= "\n" . $this->element($Element);
}
return $markup;
}
#
# Blocks
#
#
# Abbreviation
protected function blockAbbreviation($Line)
{
if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
{
$this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
$Block = array(
'hidden' => true,
);
return $Block;
}
}
#
# Footnote
protected function blockFootnote($Line)
{
if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
{
$Block = array(
'label' => $matches[1],
'text' => $matches[2],
'hidden' => true,
);
return $Block;
}
}
protected function blockFootnoteContinue($Line, $Block)
{
if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
{
return;
}
if (isset($Block['interrupted']))
{
if ($Line['indent'] >= 4)
{
$Block['text'] .= "\n\n" . $Line['text'];
return $Block;
}
}
else
{
$Block['text'] .= "\n" . $Line['text'];
return $Block;
}
}
protected function blockFootnoteComplete($Block)
{
$this->DefinitionData['Footnote'][$Block['label']] = array(
'text' => $Block['text'],
'count' => null,
'number' => null,
);
return $Block;
}
#
# Definition List
protected function blockDefinitionList($Line, $Block)
{
if ( ! isset($Block) or isset($Block['type']))
{
return;
}
$Element = array(
'name' => 'dl',
'handler' => 'elements',
'text' => array(),
);
$terms = explode("\n", $Block['element']['text']);
foreach ($terms as $term)
{
$Element['text'] []= array(
'name' => 'dt',
'handler' => 'line',
'text' => $term,
);
}
$Block['element'] = $Element;
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
protected function blockDefinitionListContinue($Line, array $Block)
{
if ($Line['text'][0] === ':')
{
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
else
{
if (isset($Block['interrupted']) and $Line['indent'] === 0)
{
return;
}
if (isset($Block['interrupted']))
{
$Block['dd']['handler'] = 'text';
$Block['dd']['text'] .= "\n\n";
unset($Block['interrupted']);
}
$text = substr($Line['body'], min($Line['indent'], 4));
$Block['dd']['text'] .= "\n" . $text;
return $Block;
}
}
#
# Header
protected function blockHeader($Line)
{
$Block = parent::blockHeader($Line);
if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
}
return $Block;
}
#
# Markup
protected function blockMarkupComplete($Block)
{
if ( ! isset($Block['void']))
{
$Block['markup'] = $this->processTag($Block['markup']);
}
return $Block;
}
#
# Setext
protected function blockSetextHeader($Line, array $Block = null)
{
$Block = parent::blockSetextHeader($Line, $Block);
if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
}
return $Block;
}
#
# Inline Elements
#
#
# Footnote Marker
protected function inlineFootnoteMarker($Excerpt)
{
if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
{
$name = $matches[1];
if ( ! isset($this->DefinitionData['Footnote'][$name]))
{
return;
}
$this->DefinitionData['Footnote'][$name]['count'] ++;
if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
{
$this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
}
$Element = array(
'name' => 'sup',
'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
'handler' => 'element',
'text' => array(
'name' => 'a',
'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
'text' => $this->DefinitionData['Footnote'][$name]['number'],
),
);
return array(
'extent' => strlen($matches[0]),
'element' => $Element,
);
}
}
private $footnoteCount = 0;
#
# Link
protected function inlineLink($Excerpt)
{
$Link = parent::inlineLink($Excerpt);
$remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';
if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
{
$Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
$Link['extent'] += strlen($matches[0]);
}
return $Link;
}
#
# ~
#
protected function unmarkedText($text)
{
$text = parent::unmarkedText($text);
if (isset($this->DefinitionData['Abbreviation']))
{
foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
{
$pattern = '/\b'.preg_quote($abbreviation, '/').'\b/';
$text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text);
}
}
return $text;
}
#
# Util Methods
#
protected function addDdElement(array $Line, array $Block)
{
$text = substr($Line['text'], 1);
$text = trim($text);
unset($Block['dd']);
$Block['dd'] = array(
'name' => 'dd',
'handler' => 'line',
'text' => $text,
);
if (isset($Block['interrupted']))
{
$Block['dd']['handler'] = 'text';
unset($Block['interrupted']);
}
$Block['element']['text'] []= & $Block['dd'];
return $Block;
}
protected function buildFootnoteElement()
{
$Element = array(
'name' => 'div',
'attributes' => array('class' => 'footnotes'),
'handler' => 'elements',
'text' => array(
array(
'name' => 'hr',
),
array(
'name' => 'ol',
'handler' => 'elements',
'text' => array(),
),
),
);
uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
{
if ( ! isset($DefinitionData['number']))
{
continue;
}
$text = $DefinitionData['text'];
$text = parent::text($text);
$numbers = range(1, $DefinitionData['count']);
$backLinksMarkup = '';
foreach ($numbers as $number)
{
$backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">&#8617;</a>';
}
$backLinksMarkup = substr($backLinksMarkup, 1);
if (substr($text, - 4) === '</p>')
{
$backLinksMarkup = '&#160;'.$backLinksMarkup;
$text = substr_replace($text, $backLinksMarkup.'</p>', - 4);
}
else
{
$text .= "\n".'<p>'.$backLinksMarkup.'</p>';
}
$Element['text'][1]['text'] []= array(
'name' => 'li',
'attributes' => array('id' => 'fn:'.$definitionId),
'text' => "\n".$text."\n",
);
}
return $Element;
}
# ~
protected function parseAttributeData($attributeString)
{
$Data = array();
$attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
foreach ($attributes as $attribute)
{
if ($attribute[0] === '#')
{
$Data['id'] = substr($attribute, 1);
}
else # "."
{
$classes []= substr($attribute, 1);
}
}
if (isset($classes))
{
$Data['class'] = implode(' ', $classes);
}
return $Data;
}
# ~
protected function processTag($elementMarkup) # recursive
{
# http://stackoverflow.com/q/1148928/200145
libxml_use_internal_errors(true);
$DOMDocument = new \DOMDocument;
# http://stackoverflow.com/q/11309194/200145
$elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
# http://stackoverflow.com/q/4879946/200145
$DOMDocument->loadHTML($elementMarkup);
$DOMDocument->removeChild($DOMDocument->doctype);
$DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
$elementText = '';
if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
{
foreach ($DOMDocument->documentElement->childNodes as $Node)
{
$elementText .= $DOMDocument->saveHTML($Node);
}
$DOMDocument->documentElement->removeAttribute('markdown');
$elementText = "\n".$this->text($elementText)."\n";
}
else
{
foreach ($DOMDocument->documentElement->childNodes as $Node)
{
$nodeMarkup = $DOMDocument->saveHTML($Node);
if ($Node instanceof \DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
{
$elementText .= $this->processTag($nodeMarkup);
}
else
{
$elementText .= $nodeMarkup;
}
}
}
# because we don't want for markup to get encoded
$DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
$markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
$markup = str_replace('placeholder\x1A', $elementText, $markup);
return $markup;
}
# ~
protected function sortFootnotes($A, $B) # callback
{
return $A['number'] - $B['number'];
}
#
# Fields
#
protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
}
+3 -4
View File
@@ -286,7 +286,7 @@ class Route
$url .= '?' . $this->getUriQuery();
}
return $url;
return rtrim($url,'/');
}
/**
@@ -344,9 +344,8 @@ class Route
$parts[] = $this->language;
}
if ($this->route !== '') {
$parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route;
}
$parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route;
if ($this->gravParams) {
$parts[] = RouteFactory::buildParams($this->gravParams);
+30 -24
View File
@@ -9,6 +9,7 @@
namespace Grav\Framework\Session;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Session\Exceptions\SessionException;
/**
@@ -17,16 +18,13 @@ use Grav\Framework\Session\Exceptions\SessionException;
*/
class Session implements SessionInterface
{
protected $options;
/** @var array */
protected $options = [];
/**
* @var bool
*/
/** @var bool */
protected $started = false;
/**
* @var Session
*/
/** @var Session */
protected static $instance;
/**
@@ -178,9 +176,13 @@ class Session implements SessionInterface
return $this;
}
$sessionName = session_name();
$sessionExists = isset($_COOKIE[$sessionName]);
// Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836
if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) {
unset($_COOKIE[session_name()]);
if ($sessionExists && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[$sessionName])) {
unset($_COOKIE[$sessionName]);
$sessionExists = false;
}
$options = $this->options;
@@ -197,24 +199,28 @@ class Session implements SessionInterface
throw new SessionException('Failed to start session: ' . $error, 500);
}
if ($user && !$user->isValid()) {
$this->clear();
throw new SessionException('User Invalid', 500);
$this->started = true;
if ($user && (!$user instanceof UserInterface || !$user->isValid())) {
$this->invalidate();
throw new SessionException('Invalid User object, session destroyed.', 500);
}
$params = session_get_cookie_params();
// Extend the lifetime of the session.
if ($sessionExists) {
$params = session_get_cookie_params();
setcookie(
session_name(),
session_id(),
time() + $params['lifetime'],
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
$this->started = true;
setcookie(
$sessionName,
session_id(),
time() + $params['lifetime'],
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
return $this;
}
+45
View File
@@ -0,0 +1,45 @@
enabled: false
built_in_css: true
name: 'Santa Claus'
title: 'Present Giver'
show_title: true
description: 'Santa Claus, Saint Nicholas, Saint Nick, Father Christmas, Kris Kringle, Santy, or simply Santa is a figure with legendary, historical and folkloric origins who, in many Western cultures, is said to bring gifts to the homes of good children on 24 December, the night before Christmas Day. The modern figure of Santa Claus is derived from the British figure of Father Christmas, the Dutch figure of Sinterklaas, and Saint Nicholas, the historical Greek bishop and gift-giver of Myra. During the Christianization of Germanic Europe, this figure may also have absorbed elements of the god Odin, who was associated with the Germanic pagan midwinter event of Yule and led the Wild Hunt, a ghostly procession through the sky'
picture_src:
user/plugins/aboutme/assets/avatars/santa.jpg:
name: santa.jpg
type: image/jpeg
size: 43391
path: user/plugins/aboutme/assets/avatars/santa.jpg
gravatar:
enabled: false
email: example@test.com
size: 100
social_pages:
enabled: true
use_font_awesome: false
pages:
facebook:
icon_type: b
icon: facebook
title: Facebook
position: 1
twitter:
icon_type: b
icon: twitter
title: Twitter
position: 2
github:
icon_type: b
icon: github
title: GitHub
position: 3
linkedin:
icon_type: b
icon: linkedin
title: LinkedIn
position: 4
instagram:
icon_type: b
icon: instagram
title: Instagram
position: 5
@@ -0,0 +1,2 @@
enabled: true
show_samples: true
+9 -7
View File
@@ -1,13 +1,15 @@
enabled: true
built_in_css: true
built_in_js: true
display_button: true
min_query_length: 3
enabled: '1'
built_in_css: '1'
built_in_js: '1'
display_button: '1'
min_query_length: '3'
route: /search
search_content: raw
template: simplesearch_results
filters:
category: ''
filter_combinator: or
ignore_accented_characters: false
ignore_accented_characters: '0'
order:
by: title
dir: desc
dir: asc
+5
View File
@@ -0,0 +1,5 @@
enabled: true
route: /sitemap
changefreq: daily
priority: !!float 1
ignores: null
+3 -5
View File
@@ -1,5 +1,5 @@
absolute_urls: false
timezone: Europe/London
timezone: Europe/Paris
param_sep: ':'
wrapped_site: false
reverse_proxy_setup: false
@@ -9,15 +9,13 @@ username_regex: '^[a-z0-9_-]{3,16}$'
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
intl_enabled: true
languages:
supported:
- en
default_lang: en
default_lang: fr
include_default_lang: false
pages_fallback_only: false
translations: false
translations_fallback: false
session_store_active: false
http_accept_language: true
http_accept_language: false
override_locale: false
home:
alias: /home
@@ -4,7 +4,7 @@ date: '08-08-2018 18:04'
date_end: '24-08-2018 18:05'
lieux: '93 Baker Street'
artistes: ' Olivia Bax'
title: 'at large, '
title: 'at large'', '
media_order: 'Olivia Bax, Boulder (with handle), 2015.jpg,Detail of Olivia Bax, Hot Spot, 2018.jpg,Olivia Bax, Love Handles, 2017.jpg,Olivia Bax, Pump, 2016.jpg,Olivia Bax, Slot _ Groove, 2015.jpg,Olivia Bax, Rumble, 2018.jpg,Installation Olivia Bax.jpg,Detail ''Footloose'', 2017.jpg,Olivia Bax, Footloose, 2017.jpg'
show_sidebar: false
aura:
@@ -2,8 +2,8 @@
date: '22-03-2019 16:06'
date_end: '03-05-2019 12:38'
lieux: '12th Floor'
title: 'Cicatrices, '
artistes: ' Muriel Abadie, Adriano Amaral, Jean-marie Appriou, Srijon Chowdhury, Isaac Lythgoe, John Miserendino, Maïa Régis, Cajsa von Zeipel'
title: 'Cicatrices'', '
media_order: 'Installation Isaac Lythgoe and Muriel Abadie.jpg,Jean-Marie Appriou, Shrimp, 2018.jpg,Muriel Abadie, Masked Monkey, 2018.jpg,John Miserendino, Mamie and Marco, 2018.jpg,Adriano Amaral, Untitled, 2018.jpg,John Miserendino, Lorigine du monde, 2018.jpg,Isaac Lythgoe, grip the wheel, look straight ahead, 2016.jpg,Details of Adriano Amaral, Untitled, 2018.jpg,Adriano Amaral, Untitled, 2018(2).jpg,Casja von Zeipel ''When?'', 2019.jpg,Adriano Amaral, untitled, 2018, Maïa Regis, Ramon Reyna, 2018.jpg,Details of Cajsa von Zeipel, When? and Why?, 2019.jpg,Cajsa von Zeipel, When? and Why?, 2019.jpg,Adriano Amaral, Untitled, 2018.jpg'
show_sidebar: false
aura:
@@ -4,7 +4,7 @@ date: '24-10-2019 17:21'
date_end: '22-11-2019 17:21'
lieux: '12th floor'
artistes: ' Dillwyn Smith'
title: 'Light Cages, '
title: 'Light Cages'', '
media_order: 'Installation 1 ''Light Cages'', Dillwyn Smith.jpg,Dillwyn Smith, ''Turning Poison into Medicine V'', 2018.jpg,Dillwyn Smith, ''Full Transparency II'', 2019, and ''Light Cage'', 2018.jpg,Dillwyn Smith, ''Sensitised Ground'', 2018.jpg,Installation 2 ''Light Cages'', Dillwyn Smith.jpg,Installation 3 ''Light Cages'', Dillwyn Smith.jpg,Dillwyn Smith, ''Full Transparency'', 2018.jpg,Dillwyn Smith, ''Hermit Song'', 2018.jpg,Dillwyn Smith, ''Working Class Hero'', 2018, and ''Praise'', 2018.jpg,Dillwyn Smith, ''Working Class Hero'', 2018.jpg'
show_sidebar: false
aura:
@@ -5,7 +5,7 @@ date_end: '20-12-2019 00:00'
display_time: false
lieux: '12th floor'
artistes: 'Mania Akbari, Douglas White '
title: 'Mr. Majidi and the Electricity Box'' '
title: 'Mr. Majidi and the Electricity Box'','
media_order: 'Installation Mania Akbari and Douglas White I.jpg,Geometric Resistance, Mania Akbari and Douglas White, 2019.jpg,Detail Mr. Majidi series, Mania Akbari and Douglas White, 2019 I.jpg,Detail Mr. Majidi series, Mania Akbari and Douglas White, 2019 II.jpg,Mr. Majidi series and House of Sin I, Mania Akbari and Douglas White, 2019.jpg,House of Sin I, Mania Akbari and Douglas White, 2019.jpg,House of Sin II, Mania Akbari and Douglas White, 2019.jpg,Installation Mania Akbari and Douglas White II.jpg,House of Sin III, Mania Akbari and Douglas White, 2019.jpg,Installation Mania Akbari and Douglas White III.jpg,Mr. Majidi series, Mania Akbari and Douglas White, 2019.jpg,Lubion, Mania Akbari and Douglas White, 2019 I.jpg,Lubion, Mania Akbari and Douglas White, 2019 II.jpg,Lubion box, Mania Akbari and Douglas White, 2019.jpg,Detail Lubion box, Mania Akbari and Douglas White, 2019.jpg'
show_sidebar: false
aura:
@@ -3,7 +3,7 @@ date: '05-09-2019 16:27'
date_end: '11-10-2019 16:27'
lieux: '12th floor'
artistes: ' Nour El Saleh'
title: 'Exquisite Farces, '
title: 'Exquisite Farces'', '
media_order: '(Not so) Musical Chairs, 2018_ Wrong Side of the Table, 2019_ The Puppeteer of Karakas, 2019.jpeg,Wrong Side of the Table, 2019.jpg,Detail of Wrong Side of the Table, 2019.jpg,A Grand Jester, 2019.jpeg,Dance Monkey, Dance!, 2019.jpg,Inhabited Props in a Box, 2018.jpeg,Detail of Inhabited Props in a Box, 2018.jpg,Four Times as Big, Twice as Small, 2018.jpg,Detail of Four Times as Big, Twice as Small, 2018.jpg'
show_sidebar: false
aura:
@@ -2,8 +2,8 @@
date: '23-05-2019 16:33'
date_end: '26-07-2019 16:33'
lieux: Phillips
title: 'Parfum d''épines,'
artistes: ' PHILLIPS X VO CURATIONS'
title: 'Parfum d''épines'','
media_order: 'Installation Daiga Grantina and Robert Brambora.jpg,Installation Jean-Marie Appriou and Claude Bellegarde.jpg,Daiga Grantina, Li Li, 2018 and Victor Man, Flowers with Skeleton and Bear Wrestling, 2012.jpg,Installation Jean-Marie Appriou, Muriel Abadie and Claude Bellegarde.jpg,Installation Oda Jaune, Pedro Wirz and Sonya Derviz.jpg,Robert Brambora, Untitled, 2019, Untitled, 2019, and 9 to 5 (2), 2018.jpg,Oda Jaune, Fleurs, 2015.jpg,Victor Man, Flowers with Skeleton and Bear Wrestling, 2012.jpg,Daiga Grantina, Yolk Bott, 2017.jpg'
show_sidebar: false
aura:
@@ -11,3 +11,19 @@ aura:
---
Phillips Auction House and V.O Curations are pleased to present a novel reading of Charles Baudelaire's collection of poems _Les Fleurs du Mal_ through the spectrum of contemporary art, exploring the seminal concept of the double postulation that has protruded beyond the literary universe of the French poet. The exhibition features the works of 9 contemporary artists that play upon the duality of the Baudelairian ethos and its fascination for antithetical pairings, the atemporal resonance of what the poet refers to as the "two inseparable abysses of the human soul". Parfum d'épines was supported by Parcours 2019 and Outset Contemporary Art Fund.
--
Charles Baudelaire a bâti son univers poétique dans l'atmosphère parisienne des années 1850, une période pendant laquelle la misère coexistait avec la plus grande des sophistications, où la ville de Paris a connu de drastiques transformations urbaines. Flânant le long des avenues Haussmanniennes éclairées aux becs de gaz, le poète regrette le passé. Pourtant c'est cette modernité émergente, son dynamisme et sa vulgarité qui l'ont conduit à développer une nouvelle forme de poésie. Fervent catholique, auteur de scandaleux blasphèmes qui lui ont valu d'être condamné et censuré, Charles Baudelaire a fait de la dualité son axiome fondamental, étant lui-même divisé entre la quête du Beau et l'inévitable abysse du péché. Puisant dans la tendresse et la haine, le parfum et les épines, Les Fleurs du Mal - recueil publié en 1857 - sont telle une prouesse de la Beauté inextricablement liée au Mal. La cohésion de ces polarités permet de faire sens dans des directions opposées sans pour autant les disjoindre. En considérant la paire antithétique comme un tout, Baudelaire forge la notion de double postulation, cœur de son univers littéraire. L'exposition Parfum d’épines cherche à en révéler l'écho dans le domaine de l'art contemporain.
Les sculptures abstraites et composites de Daiga Grantina ressemblent à des organes palpitants, des pans de chairs dont le plasma et les fluides vitaux auraient été drainés. Elles sapparentent à une version plus asséchée des viscères du "ventre putride" de la Charogne. Tulle, coton, mousse et silicone : les tissus ondulés se déversent dans l'espace et déploient un univers de signification riche suggérant à la fois le végétal, le médical et le délicat. Ces plis voluptueux sont semblables à des amphores elles-mêmes ressemblant à un intestin. On entend résonner les vers de Baudelaire dans lequel le souvenir d'un cadavre devient une source de création esthétique: " et le ciel regardait la carcasse superbe comme une fleur s'épanouir ". De la même façon, le bouquet d'entrailles d'Oda Jaune rapproche le floral de l'intestinal. Les organes sanglants qu'elle représente sont plus réalistes que ceux de Daiga Grantina en ce qu'ils possèdent encore leurs fluides vitaux qui permettent au cadavre de fleurir chez Baudelaire, à une photosynthèse corporelle d'advenir. Aussi, après l'extraction de ces fleurs de sang, le bouquet de viscères, est maintenu en vie extra-corporellement. Sous le prisme baudelairien se laisse entrevoir tout un imaginaire de l'amour dévorant et des "amours décomposés".
Dans le bouquet de Victor Man, réminiscence des fleurs maladives du poète, la vie semble se cacher sous un masque mortuaire: les fleurs de chardon apparaissent fanées alors qu'elles sont en pleine floraison. Au lieu d'être parées de leur naturelle teinte améthyste, elles portent le gris. La fleur de chardon, utilisée comme un aphrodisiaque à l'époque Élisabéthaine, couplée avec le squelette, évoque les memento mori, ces peintures illustrant la locution latine " souviens toi que tu vas mourir ". Elles réunissent les symboles de deux pulsions majeures, à savoir l'Eros et le Thanatos, que Baudelaire convoque quand, dans Femmes Damnées (Delphine et Hippolyte), il trouve sur un sein "la fraîcheur des tombeaux !".
Les profils aplatis de Robert Brambora ne privilégient pas le rendu sculptural du modèle mais plutôt leur être émotionnel. Ainsi, ce qui est normalement intime se trouve dévoilé, l'extérieur et l'intérieur se confondent en des paysages de soleils et de nuages. Si l'on établit une référence avec les Correspondances de Baudelaire, l'œuvre de Brambora nous fait voir le monde naturel comme "un temple où de vivants piliers laissent parfois sortir de confuses paroles". Dans ce monde, véritable " forêts de symboles ", l'humain est étroitement lié à la flore et à la faune.
La fascination de Pedro Wirz pour la métamorphose des amphibiens - de leur apparence à leur habitat, passant de l'œuf à la grenouille, de l'eau à la terre imprègne sa production artistique. Marqué par les mythes et les rituels brésiliens auxquels il a été exposé dans l'enfance, l'artiste traite l'animal avec une dimension spirituelle et l'élève, peut-être, au rang d'alter ego. En plus de cette notion de dédoublement, on trouve également dans ses œuvres, le rapport d'inversion et le devenir autre explicité par la grenouille moulée en cire d'abeille.
En employant l'aluminium, Jean-Marie Appriou transforme un matériau industriel en matière organique, créant son propre microenvironnement ; cyprès, chauve-souris et serpent peuplent ce monde métallique. Un certain mysticisme se trouve entremêlé à une ténébreuse perception. Le cyprès symbolise à la fois la mort et l'aspiration ascétique et le serpent expulsé du Paradis nous rappelle l'indolente tentatrice du Serpent qui Danse. Après avoir été confiné aux enfers, une chauve-souris baudelairienne s'élance et surgit dans la lumière cherchant éperdument l'Espoir et sera finalement incapable de quitter sa grotte. Si la chauve-souris de Jean-Marie Appriou se bat pour s'élever, l'oiseau de Muriel Abadie choit vertigineusement comme l'Albatros de Baudelaire. À travers l'énergie conférée par la forme esquissée, Muriel Abadie a transposé le dynamisme de l'oiseau, sa liberté et sa mobilité à l'âme du poète qui mène à la fois une existence terrestre et céleste. Chutant vers le sol, peut-être même déjà retenu par lui, le vol est interrompu et la maison vide ressemble à une cage. Entre l'ascension et la chute, cygne ou albatros, le poète n'est jamais aussi éloigné des Hommes que quand il est parmi eux. La figure fantomatique de Sonya Derviz quant à elle, reflète la dualité de la condition humaine: multitude de facettes en une seule entité.
Les achromes de Claude Bellegarde encerclent cet inhérent jeu de doubles à travers l'emploi du blanc, une non-couleur ayant le potentiel d'être néant et origine, une blancheur féconde, un point zéro de l'espace qui n'est plus soumis au despotisme de la perspective. Dans le travail de l'artiste, les plis, les larmes et les rides affirment leur corporéité et donnent l'impression d'une matière travaillée vivante tel un papier froissé que Baudelaire assimile à une compression du cœur.
@@ -3,7 +3,7 @@ date: '05-08-2018 16:37'
date_end: '18-08-2018 16:37'
lieux: '93 Baker Street '
artistes: ' Sonya Derviz'
title: 'Pas de Corps, '
title: 'Pas de Corps'', '
media_order: 'Sonya Derviz, Dancers II, 2018.jpg'
show_sidebar: false
aura:
@@ -3,7 +3,7 @@ date: '03-10-2018 17:49'
date_end: '19-10-2018 17:49'
lieux: '93 Baker Street '
artistes: ' Sara Berman'
title: 'Matter out of Place,'
title: 'Matter out of Place'','
media_order: 'Sara Berman, Lint Sweater #40, 2018.jpg,Sara Berman, Lint Sweater #37, 2018.jpg,Installation 1 Sara Berman.jpg,Installation 2 Sara Berman.jpg,Sara Berman, Lint Sweater #38, 2018.jpg'
show_sidebar: false
aura:
@@ -2,8 +2,8 @@
date: '03-07-2018 16:39'
date_end: '13-07-2018 16:40'
lieux: '93 Baker Street '
title: 'URBAN WAVES, '
artistes: ' Andrei Costache, Richie Culver an­d Morgan Ward'
title: 'URBAN WAVES'', '
media_order: 'Andrei Costache, Tarantula, 2018.jpg'
show_sidebar: false
aura:
@@ -1,13 +1,28 @@
---
topH: true
topH: false
date: '13-02-2020 10:31'
date_end: '28-03-2020 10:31'
lieux: '12th floor'
artistes: 'Sola Olulode'
title: 'Where The Ocean Meets The Beach'' '
title: 'Where The Ocean Meets The Beach'','
media_order: 'Installation. Sola Olulode. In the Middle. The Feels and A Perfect Summer''s Day.jpeg,Sola Olulode. The Feels and A Perfect Summer''s Day. 2019.jpeg,Sola Olulode. The Feels. 2019.jpeg,Sola Olulode. A Perfect Summer''s Day. 2019.jpeg,Sola Olulode. Gals Pals. 2019.jpg,Sola Olulode. Close To You. 2019.jpeg,Installation. Sola Olulode. I Could Just Lay All Day With You and Laying in the Grass.jpeg,Sola Olulode. I Could Just Lay All Day With You. 2019.jpg,Sola Olulode. Laying in The Grass. 2019.jpg,Installation. Sola Olulode. Laying in the Grass. Entwined and Eternal Light.jpeg,Installation. Sola Olulode. Safety and Entwined.jpeg,Sola Olulode. Safety. 2019.jpeg,Sola Olulode. Entwined. 2020.jpeg,Sola Olulode. Eternal Light. 2020.jpeg,Sola Olulode. Waves. 2020.jpeg'
show_sidebar: false
aura:
pagetype: website
---
Sola Olulode's end of residency solo show _Where the Ocean Meets the Beach_' will take place at our 12th floor gallery from 13th February until 28th March. Olulode has been working on a new series of paintings in which narratives of queer and Black identity culminate in the representation of romantic relationships an their complexities. Taken from artist Travis Alabanza's poem _The Sea_, the exhibition title refers to Alabanzas feeling on gender fluidity as a boundless place similar to 'where the ocean meets the beach'. Analogously to Alabanzas sentiment, Olulode renders the fluidity of emotions that chronicle the falling in love.
_Where the Ocean Meets the Beach_ is Sola Olulodes end of residency show at V.O Curations, a three-month period dedicated to Olulodes research into representations of QTIBPOC (Queer Trans Intersex Black People & People of Colour) and romance. The artist presents a new series of paintings where narratives of Black queer womxn culminate in the representation of romantic relationships and their complexities, directly drawing upon Olulodes experiences. Focusing on love, Olulode delves into the intimacy, complicity of dating and the impassioned honeymoon phase.
Taken from artist Travis Alabanza's poem _The Sea_, the exhibition title refers to Alabanzas feeling on gender fluidity as a boundless place similar to where the ocean meets the beach. Analogously to the poets sentiment, Olulode renders the fluidity of emotions that chronicles the process of falling in love. The constant malleable shift and undefinable identity, a central theme of Alabanzas poem, enhances and sets the stage for Olulodes characters to tell the untold, but timeless, stories that remain crystallised in calming moments of reflection and joy.
By creating a space that encapsulates the lovers, Olulode transports them into a placid and tranquil bubble that transcends reality. Exploring love as a protective barrier, a refuge from the misrepresentation and the confinement within a heteronormative ideal, that both denies and depreciates queer relationships. Reflecting the intimate and complex relationships of Black queer womxn, be they romantic or deeply platonic, Olulode questions the dynamics of the same existence of these relationships. The lovers genders are undefined and devoid of eroticity, straying away from a grazing patriarchal gaze of male desire and traditional heterosexual relational aesthetics. The figures are intrinsically naive and exude an aura of innocence, parting from references to eroticisim and crude sexuality, which queer relationships often become victim to. The perspective offered is delicate, creeping softly into a space, where only the deeply profound bond remains.
_Safety_ (2019) represents Olulodes depiction of a safe and intimate space for the lovers resting under the duvet covers; inundated by a veil of indigo, the painting recalls Olulodes experimentation with the Adire technique, a dyed textile made by the Yoruba people in Nigeria. Dyeing textiles and the use of melted wax directly references the artists Nigerian roots, echoing and informing her oeuvre through the lens of her background and history.
From the deeper tones that evoke the atmosphere and intimacy of nighttime, to the warmer hues that veil daily interactions between the lovers, indigo and turmeric envelop the characters and almost blend with them, becoming both part and background for them to exist. _A Perfect Summers Day_ (2019) amalgamates the lovers in a honey- glazed lens, where they lay imperturbable, suspended in a dimension without time nor space.
Olulode emphasises the undefinability of the characters and their gender, hair and clothing remaining the only signifier of personal identity. Curly afros, twists, locs with thick deep black lines and cornrows: the appearance of detailed hairstyles allows for more agency and reality to each figure. The intricate details are delicately interwoven and applied, either sewn or drawn upon, generating unique patterns that become an identifiable detail within her works. The lovers in _Good Vibrations _(2020) are distinct from other figures as their clothing becomes a prominent part of their depiction, with polka dots and pinstripes gently laying down on their bodies and raising them from the background where they emerge from.
_British-Nigerian artist Sola Olulode (b. 1996, London) graduated from the University of Brighton in 2018. After successfully completing a residency at Lewisham Art House, she was recently shortlisted for the Evening Standard Art Prize, and featured in V&As 2020 exhibition In the Palm of Your Hands . Olulode has been developing a narrative investigating queerness and black identity through the lens of social interactions. _
Accessibility entrance from 39 York Road. For further assistance please call +447789813313.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

@@ -1,14 +1,22 @@
---
topH: true
topH: false
date: '09-01-2020 17:50'
date_end: '01-02-2020 17:50'
lieux: '12th floor'
artistes: 'Frances Gibson, Alexandre Canonico, André Figueiredo da Silva, Josephine Baker'
title: '''Wreck Or Ruin'''
media_order: 'Josephine Baker and Alexandré Canonico.jpeg,Alexandre Canonico Through 2020.jpeg,Alexandre Canonico Through (detail) 2020.jpeg,Josephine Baker Abrasion Coast 2020.jpeg,Josephine Baker Abrasion Coast (detail) 2020.jpeg,Josephine Baker Waterfall 2020.jpeg,Josephine Baker Waterfall (detail) 2020.jpeg,Installation Frances Gibson Alexandre Canonico André Figueiredo da Silva.jpeg,Frances Gibson A Pound of Flower 2020.jpeg,Frances Gibson 40 Elephants 2020.jpeg,Alexandre Canonico Dig #7 2019.jpeg,André Figueiredo da Silva Pinning Down 2020.jpeg,André Figueiredo da Silva Pinning Down (detail) 2020.jpeg'
title: 'Wreck Or Ruin,'
media_order: 'Installation. Alexandre Canonico and Jospehine Baker.jpeg,Josephine Baker. Abrasion Coast. 2020.jpeg,Josephine Baker. Abrasion Coast (detail). 2020.jpeg,Installation. Alexandre Canonico and Josephine Baker.jpeg,Josephine Baker. Waterfall. 2020.jpeg,Josephine Baker. Waterfall (detail). 2020.jpeg,Alexandre Canonico. Through. 2020.jpeg,Alexandre Canonico. Through (detail). 2020.jpeg,Installation. Frances Gibson. Alexandre Canonico. André Figueiredo da Silva.jpeg,Frances Gibson. A Pound of Flower. 2020.jpeg,Frances Gibson. 40 Elephants. 2020.jpeg,Frances Gibson. 40 Elephants (detail). 2020.jpeg,Alexandre Canonico. Dig #7. 2019.jpeg,André Figueiredo da Silva. Pinning Down. 2020.jpeg'
show_sidebar: false
aura:
pagetype: website
---
We are thrilled to host Wreck or Ruin, a collaborative project of sculptural works that has organically developed between André Figueiredo da Silva, Alexandre Canonico, Josephine Baker and Frances Gibson at 12th floor, reflecting upon the impending demolition of Tower Building.
Wreck or Ruin is a collaborative project that has organically developed between André Figueiredo da Silva, Alexandre Canonico, Josephine Baker and Frances Gibson at 12th floor, reflecting upon the impending demolition of Tower Building.
As a studio resident in the Tower Building, Figueiredo da Silva incorporates architectural elements from the site: corners, walls, columns. Pink sculptural foamed columns act as traces of a building that will cease to exist. As the foam cannot sustain anything in its usual form, Figueiredo da Silvas structure relies on carefully depressurised footballs that require a specific maintenance, similar to that of the building. By misusing the materials and turning them into something they are not meant to be, the artist makes an allusion to the building's present state of instability.
Using materials as a starting point, Canonico has taken a playful approach to create sculptures from commonly found objects. Metal rods and hoses sit together, sustained by the interplay between their different material qualities to bridge the space between floor and ceiling. While Canonicos works are not site-specific, they rely on the negative space surrounding them. The sculptures embrace the air they encapsulate.
Artist Josephine Baker has created relief works that mimic the historical production of nature and landscape as images or grand designs of humancentric relations. Their diagrammatic and almost pixelated motifs, made out of building and industrial materials, appear to illustrate elemental processes such as water cycles and the movements of the earth. Her works further comment on the lack of distinction between human-made and natural disasters in today's world.
Finally, Gibsons work takes the form of a memorial of forty small clay elephants and a sculpture of a pound symbol made of carnations. The small sculptures reference the infamous all-women crime syndicate The Forty Elephants (based in Elephant and Castle), who conducted the largest shoplifting operation in Britain during their activity from the 1870s to 1950s. Gibsons work for the project is informed by a tenuous chain of events and research based on the history of the building.
@@ -83,6 +83,6 @@ form:
reset: true
---
We accept applications of artists from all practices. Our studios can be used individually or shared with others; applications are for one person at a time, so even if you are applying as a group, please fill out individual applications and specify in _notes_ who you wish to share with. Our studio prices start from £100pm and are offered inline with _Greater London Authority's affordability guide (2014, 2019)_. All prices are inclusive of utilities (inc. wifi). Facilities are accessible 24/7 with communal areas and shared kitchens available.
We accept applications of artists from all practices. Our studios can be used individually or shared with others; applications are for one person at a time, even if you apply as a group, please fill out individual applications and specify in _notes_ who you wish to share with. Our studio prices start from £100pm and are offered inline with _Greater London Authority's affordability guide (2014, 2019)_. All prices are inclusive of utilities. Facilities are accessible 24/7 with communal areas and shared kitchens available.
Please allow us a week to get back to you, you will receive a response via email. Feel free to contact [studio@vocurations.com](mailto:studio@vocurations.com) for any further questions, or to organise a site visit!
Feel free to contact [studio@vocurations.com](mailto:studio@vocurations.com) for any further questions, or to organise a site visit.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

@@ -1,11 +1,12 @@
---
title: About
media_order: 'VO Curations - 12th Floor.jpg'
media_order: 'Anousha Payne.jpg,SJM_VO_SonnyShanti-9451.jpg,SJM_Vo-9154.jpg,Portrait Azadeh and Salman.jpg,_MG_2961 (2).jpg'
visible: false
aura:
pagetype: website
---
V.O Studios offers affordable workspaces to artists based in London. The group reflects the diversity of artists and practices in the city, and form an integral part of V.Os community-at-large. All studios are located in Zone 1 Central London, with the view to ease access and visibility for each artist. Open Days are scheduled throughout the year to offer artists ongoing opportunities to explore new dialogues with interested members of the local community, as well as the wider public. V.O ensures their artist community their ongoing support through advising on professional development, mentoring, presentation, and access to workshop and exhibition opportunities. V.O Studios are currently located in Waterloo, SE1 and 242 Marylebone Road, NW1, with more upcoming spaces from spring 2020!
V.O Studios offers affordable workspaces to artists based in London. The group reflects the diversity of artists and practices in the city, and form an integral part of V.Os community-at-large. All studios are located in Zone 1 Central London, with the view to ease access and visibility for each artist. Open Days are scheduled throughout the year to offer artists ongoing opportunities to explore new dialogues with interested members of the local community, as well as the wider public. V.O ensures their artist community their ongoing support through advising on professional development, mentoring, presentation, and access to workshop and exhibition opportunities. V.O Studios are currently located in 242 Marylebone Road, NW1, with more upcoming spaces from Autumn 2020.
_V.O Studios is a not-for-profit initiative launched October 2019._
@@ -5,7 +5,7 @@ show_sidebar: false
content:
items:
- '@self.children'
limit: 5
limit: 999
order:
by: date
dir: desc
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

@@ -3,11 +3,11 @@ residence: true
date: '12-12-2019 13:22'
date_end: '27-02-2020 13:22'
title: 'Emma Prempeh '
media_order: 20190611_090033694_iOS.jpg
media_order: 'Example Vo.jpg'
published: true
show_sidebar: false
aura:
pagetype: website
---
Recent graduate from Goldsmiths University, Emma Prempeh is currently an artist-in-residence at V.O Curations, completing a new series of work which will be exhibited with V.O Curations in Spring 2020.
Artist Emma Prempeh (b.1996, London) is a fine art graduate from Goldsmiths University and is currently attending an MA in Painting at the Royal College of Art. Family and generational continuity is often the subject of Prempehs paintings, as relational ties are explored and questioned, through the depiction of her mother and grandmother and their experiences. The search of spirituality enables Prempeh to analyse existential questions that are projected upon her reality; the fear of death, memory, ancestral ties. After completing a 2-month residency at V.O Curations, Prempeh has debuted with solo show The Faces of Love (13.10-14.11.2020) held at V.O Studios, 242 Marylebone Road.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

@@ -1,12 +1,11 @@
---
date: '14-11-2019 14:54'
date_end: '14-02-2020 14:54'
title: 'Sola Olulode '
media_order: 'Sola Olulode at V.O Curations.JPG'
date: '14-11-2019 10:34'
date_end: '14-02-2020 10:34'
title: 'Sola Olulode'
media_order: 'Sola Olulode at VO Curations.jpg'
show_sidebar: false
aura:
pagetype: website
date_start: '14-11-2019 10:46'
---
British-Nigerian artist Sola Olulode is currently completing a three-month residency at V.O Curations working on a new series of paintings where narratives of queer and Black identity culminate in the representation of romantic relationships and their complexities. The artist's end of residency solo show 'Where the ocean meets the beach' will take place at V.O's 12th floor gallery from 13 February to 28 March.
British-Nigerian artist Sola Olulode (b. 1996, London) is a fine art graduate from the University of Brighton. Olulode explores the relations of QTIBPOC (Queer Trans Intersex Black People & People of Colour) and black identity through the lens of social interactions, working with wax and textile dyes taken from Yoruba tradition. Sola Olulode has been in a 3-month residency at V.O Curations, culminating in the solo show Where the Ocean Meets the Beach (13.02-28.03-2020) at V.O Curations, 12th Floor.
@@ -1,11 +0,0 @@
---
date_start: '05-06-2019 11:05'
date_end: '05-09-2019 19:43'
title: 'Nour El Saleh'
media_order: 'Nour El Saleh, Detail ''Four Times as Big, Twice as Small'' 2018.jpeg'
date: '05-06-2019 19:43'
show_sidebar: false
aura:
pagetype: website
---
+1
View File
@@ -1,5 +1,6 @@
---
title: 'In residence'
media_order: 'Emma Prempeh Studio Photo 2.JPG'
redirect: /residence/about
show_sidebar: false
content:
-1
View File
@@ -14,4 +14,3 @@ content:
aura:
pagetype: website
---
@@ -1,9 +1,9 @@
---
title: 'Phillips x VO Curations Presents Perfume Of Thorns'
date_event: '23-05-2019 19:30'
date: '23-05-2019 19:30'
aura:
pagetype: website
date_event: '23-05-2019 19:30'
---
[Phillips x VO Curations Presents Perfume Of Thorns, Outset Contemporary](https://outset.org.uk/supported-projects/phillips-x-vo-curations-presents-perfume-of-thorns/)
@@ -1,10 +0,0 @@
---
title: 'Sam Cornish on Olivia Bax'
date_event: '16-08-2018 17:31'
date: '16-08-2018 17:31'
aura:
pagetype: website
class: small
---
['Sam Cornish on Olivia Bax', Instantloveland](https://instantloveland.com/wp/2018/08/16/sam-cornish-on-olivia-bax/)
@@ -1,9 +0,0 @@
---
title: 'Paintings by Sola Olulode explore the ''honeymoon'' phase of romance'
date_event: '16-01-2020 15:38'
date: '16-01-2020 15:38'
aura:
pagetype: website
---
['Paintings by Sola Olulode explore the ''honeymoon'' phase of romance', Creative Boom](https://www.creativeboom.com/inspiration/paintings-by-sola-olulode-explore-the-honeymoon-phase-of-romance/)
@@ -1,11 +0,0 @@
---
title: 'Adriano Amaral, Et Al., ''Euphoria'', Tick Tack, Antwerpen, Belgium'
media_order: 'Adriano Amaral, Untitled, 2018.jpg'
date: '09-11-2019 14:49'
date_end: '21-12-2019 14:50'
url_news: 'https://www.ticktack.be/exhibitions/euphoria-curated-by-domenico-de-chirico'
published: true
aura:
pagetype: website
---
@@ -1,11 +0,0 @@
---
title: 'Daiga Grantina, ''What Eats Around Itself'', New Museum, New York, USA'
media_order: 'Daiga Grantina, ''What Eats Around Itself'', New Museum.png'
date: '21-01-2020 14:41'
date_end: '05-09-2020 14:41'
url_news: 'https://www.newmuseum.org/exhibitions/view/daiga-grantina-what-eats-around-itself'
published: true
aura:
pagetype: website
---
@@ -1,6 +1,6 @@
---
title: 'Isaac Lythgoe, ''Railway Spine'', Super Dakota, Brussels, Belgium'
media_order: 'Isaac Lythgoe ''Railway Spine''.jpg'
media_order: 'Screenshot 2020-01-23 at 16.50.04.png'
date: '09-01-2020 14:20'
date_end: '25-02-2020 14:21'
url_news: 'http://www.superdakota.com/exhibitions/isaac-lythgoe-railway-spine'
@@ -1,11 +0,0 @@
---
title: 'Jean-Marie Appriou, Biennale de Lyon, Lyon, France'
date: '18-09-2019 19:52'
date_end: '05-01-2020 19:52'
url_news: 'https://www.biennaledelyon.com/en/artistes/jean-marie-appriou-2/'
published: true
aura:
pagetype: website
show_sidebar: false
---
@@ -2,7 +2,7 @@
title: 'Jean-Marie Appriou, ''The Horses'', Central Park, New York, USA'
media_order: 'Jean-Marie Appriou _The Horses_, Central Park, New York, USA .jpg'
date: '11-09-2019 19:55'
date_end: '15-12-2020 19:55'
date_end: '30-08-2020 19:55'
url_news: 'https://www.publicartfund.org/view/exhibitions/6708_jean-marie_appriou_the_horses'
published: false
aura:

Some files were not shown because too many files have changed in this diff Show More