diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 00000000..efdf5b1a --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,44 @@ + null, + 'pretend_newer_core_functions_exist' => true, + 'allow_missing_properties' => false, + 'null_casts_as_any_type' => false, + 'null_casts_as_array' => false, + 'array_casts_as_null' => false, + 'strict_method_checking' => true, + 'quick_mode' => false, + 'simplify_ast' => false, + 'directory_list' => [ + '.', + ], + "exclude_analysis_directory_list" => [ + 'vendor/' + ], + 'exclude_file_list' => [ + 'system/src/Grav/Common/Errors/Resources/layout.html.php', + 'tests/_support/AcceptanceTester.php', + 'tests/_support/FunctionalTester.php', + 'tests/_support/UnitTester.php', + ], + 'autoload_internal_extension_signatures' => [ + 'memcached' => '.phan/internal_stubs/memcached.phan_php', + 'memcache' => '.phan/internal_stubs/memcache.phan_php', + 'redis' => '.phan/internal_stubs/Redis.phan_php', + ], + 'plugins' => [ + 'AlwaysReturnPlugin', + 'UnreachableCodePlugin', + 'DuplicateArrayKeyPlugin', + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + ], + 'suppress_issue_types' => [ + 'PhanUnreferencedUseNormal', + 'PhanTypeObjectUnsetDeclaredProperty', + 'PhanTraitParentReference', + 'PhanTypeInvalidThrowsIsInterface', + 'PhanRequiredTraitNotAdded', + 'PhanDeprecatedFunction', // Uncomment this to see all the deprecated calls + ] +]; diff --git a/.phan/internal_stubs/Redis.phan_php b/.phan/internal_stubs/Redis.phan_php new file mode 100644 index 00000000..ed349f23 --- /dev/null +++ b/.phan/internal_stubs/Redis.phan_php @@ -0,0 +1,5153 @@ + + * @link https://github.com/ukko/phpredis-phpdoc + */ +class Redis +{ + const AFTER = 'after'; + const BEFORE = 'before'; + + /** + * Options + */ + const OPT_SERIALIZER = 1; + const OPT_PREFIX = 2; + const OPT_READ_TIMEOUT = 3; + const OPT_SCAN = 4; + const OPT_SLAVE_FAILOVER = 5; + + /** + * Cluster options + */ + const FAILOVER_NONE = 0; + const FAILOVER_ERROR = 1; + const FAILOVER_DISTRIBUTE = 2; + + /** + * SCAN options + */ + const SCAN_NORETRY = 0; + const SCAN_RETRY = 1; + + /** + * Serializers + */ + const SERIALIZER_NONE = 0; + const SERIALIZER_PHP = 1; + const SERIALIZER_IGBINARY = 2; + const SERIALIZER_MSGPACK = 3; + const SERIALIZER_JSON = 4; + + /** + * Multi + */ + const ATOMIC = 0; + const MULTI = 1; + const PIPELINE = 2; + + /** + * Type + */ + const REDIS_NOT_FOUND = 0; + const REDIS_STRING = 1; + const REDIS_SET = 2; + const REDIS_LIST = 3; + const REDIS_ZSET = 4; + const REDIS_HASH = 5; + + /** + * Creates a Redis client + * + * @example $redis = new Redis(); + */ + public function __construct() + { + } + + /** + * Connects to a Redis instance. + * + * @param string $host can be a host, or the path to a unix domain socket + * @param int $port optional + * @param float $timeout value in seconds (optional, default is 0.0 meaning unlimited) + * @param null $reserved should be null if $retryInterval is specified + * @param int $retryInterval retry interval in milliseconds. + * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited) + * + * @return bool TRUE on success, FALSE on error + * + * @example + *
+ * $redis->connect('127.0.0.1', 6379);
+ * $redis->connect('127.0.0.1'); // port 6379 by default
+ * $redis->connect('127.0.0.1', 6379, 2.5); // 2.5 sec timeout.
+ * $redis->connect('/tmp/redis.sock'); // unix domain socket.
+ *
+ */
+ public function connect(
+ $host,
+ $port = 6379,
+ $timeout = 0.0,
+ $reserved = null,
+ $retryInterval = 0,
+ $readTimeout = 0.0
+ ) {
+ }
+
+ /**
+ * Connects to a Redis instance.
+ *
+ * @param string $host can be a host, or the path to a unix domain socket
+ * @param int $port optional
+ * @param float $timeout value in seconds (optional, default is 0.0 meaning unlimited)
+ * @param null $reserved should be null if $retry_interval is specified
+ * @param int $retryInterval retry interval in milliseconds.
+ * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited)
+ *
+ * @return bool TRUE on success, FALSE on error
+ *
+ * @see connect()
+ * @deprecated use Redis::connect()
+ */
+ public function open(
+ $host,
+ $port = 6379,
+ $timeout = 0.0,
+ $reserved = null,
+ $retryInterval = 0,
+ $readTimeout = 0.0
+ ) {
+ }
+
+ /**
+ * A method to determine if a phpredis object thinks it's connected to a server
+ *
+ * @return bool Returns TRUE if phpredis thinks it's connected and FALSE if not
+ */
+ public function isConnected()
+ {
+ }
+
+ /**
+ * Retrieve our host or unix socket that we're connected to
+ *
+ * @return string|bool The host or unix socket we're connected to or FALSE if we're not connected
+ */
+ public function getHost()
+ {
+ }
+
+ /**
+ * Get the port we're connected to
+ *
+ * @return int|bool Returns the port we're connected to or FALSE if we're not connected
+ */
+ public function getPort()
+ {
+ }
+
+ /**
+ * Get the database number phpredis is pointed to
+ *
+ * @return int|bool Returns the database number (int) phpredis thinks it's pointing to
+ * or FALSE if we're not connected
+ */
+ public function getDbNum()
+ {
+ }
+
+ /**
+ * Get the (write) timeout in use for phpredis
+ *
+ * @return float|bool The timeout (DOUBLE) specified in our connect call or FALSE if we're not connected
+ */
+ public function getTimeout()
+ {
+ }
+
+ /**
+ * Get the read timeout specified to phpredis or FALSE if we're not connected
+ *
+ * @return float|bool Returns the read timeout (which can be set using setOption and Redis::OPT_READ_TIMEOUT)
+ * or FALSE if we're not connected
+ */
+ public function getReadTimeout()
+ {
+ }
+
+ /**
+ * Gets the persistent ID that phpredis is using
+ *
+ * @return string|null|bool Returns the persistent id phpredis is using
+ * (which will only be set if connected with pconnect),
+ * NULL if we're not using a persistent ID,
+ * and FALSE if we're not connected
+ */
+ public function getPersistentID()
+ {
+ }
+
+ /**
+ * Get the password used to authenticate the phpredis connection
+ *
+ * @return string|null|bool Returns the password used to authenticate a phpredis session or NULL if none was used,
+ * and FALSE if we're not connected
+ */
+ public function getAuth()
+ {
+ }
+
+ /**
+ * Connects to a Redis instance or reuse a connection already established with pconnect/popen.
+ *
+ * The connection will not be closed on close or end of request until the php process ends.
+ * So be patient on to many open FD's (specially on redis server side) when using persistent connections on
+ * many servers connecting to one redis server.
+ *
+ * Also more than one persistent connection can be made identified by either host + port + timeout
+ * or host + persistentId or unix socket + timeout.
+ *
+ * This feature is not available in threaded versions. pconnect and popen then working like their non persistent
+ * equivalents.
+ *
+ * @param string $host can be a host, or the path to a unix domain socket
+ * @param int $port optional
+ * @param float $timeout value in seconds (optional, default is 0 meaning unlimited)
+ * @param string $persistentId identity for the requested persistent connection
+ * @param int $retryInterval retry interval in milliseconds.
+ * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited)
+ *
+ * @return bool TRUE on success, FALSE on ertcnror.
+ *
+ * @example
+ *
+ * $redis->pconnect('127.0.0.1', 6379);
+ *
+ * // port 6379 by default - same connection like before
+ * $redis->pconnect('127.0.0.1');
+ *
+ * // 2.5 sec timeout and would be another connection than the two before.
+ * $redis->pconnect('127.0.0.1', 6379, 2.5);
+ *
+ * // x is sent as persistent_id and would be another connection than the three before.
+ * $redis->pconnect('127.0.0.1', 6379, 2.5, 'x');
+ *
+ * // unix domain socket - would be another connection than the four before.
+ * $redis->pconnect('/tmp/redis.sock');
+ *
+ */
+ public function pconnect(
+ $host,
+ $port = 6379,
+ $timeout = 0.0,
+ $persistentId = null,
+ $retryInterval = 0,
+ $readTimeout = 0.0
+ ) {
+ }
+
+ /**
+ * @param string $host
+ * @param int $port
+ * @param float $timeout
+ * @param string $persistentId
+ * @param int $retryInterval
+ * @param float $readTimeout
+ *
+ * @return bool
+ *
+ * @deprecated use Redis::pconnect()
+ * @see pconnect()
+ */
+ public function popen(
+ $host,
+ $port = 6379,
+ $timeout = 0.0,
+ $persistentId = '',
+ $retryInterval = 0,
+ $readTimeout = 0.0
+ ) {
+ }
+
+ /**
+ * Disconnects from the Redis instance.
+ *
+ * Note: Closing a persistent connection requires PhpRedis >= 4.2.0
+ *
+ * @since >= 4.2 Closing a persistent connection requires PhpRedis
+ *
+ * @return bool TRUE on success, FALSE on error
+ */
+ public function close()
+ {
+ }
+
+ /**
+ * Swap one Redis database with another atomically
+ *
+ * Note: Requires Redis >= 4.0.0
+ *
+ * @param int $db1
+ * @param int $db2
+ *
+ * @return bool TRUE on success and FALSE on failure
+ *
+ * @link https://redis.io/commands/swapdb
+ * @since >= 4.0
+ * @example
+ * + * // Swaps DB 0 with DB 1 atomically + * $redis->swapdb(0, 1); + *+ */ + public function swapdb(int $db1, int $db2) + { + } + + /** + * Set client option + * + * @param int $option option name + * @param mixed $value option value + * + * @return bool TRUE on success, FALSE on error + * + * @example + *
+ * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); // don't serialize data + * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); // use built-in serialize/unserialize + * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); // use igBinary serialize/unserialize + * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK); // Use msgpack serialize/unserialize + * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); // Use json serialize/unserialize + * + * $redis->setOption(Redis::OPT_PREFIX, 'myAppName:'); // use custom prefix on all keys + * + * // Options for the SCAN family of commands, indicating whether to abstract + * // empty results from the user. If set to SCAN_NORETRY (the default), phpredis + * // will just issue one SCAN command at a time, sometimes returning an empty + * // array of results. If set to SCAN_RETRY, phpredis will retry the scan command + * // until keys come back OR Redis returns an iterator of zero + * $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY); + * $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); + *+ */ + public function setOption($option, $value) + { + } + + /** + * Get client option + * + * @param int $option parameter name + * + * @return mixed|null Parameter value + * + * @see setOption() + * @example + * // return option value + * $redis->getOption(Redis::OPT_SERIALIZER); + */ + public function getOption($option) + { + } + + /** + * Check the current connection status + * + * @return string STRING: +PONG on success. + * Throws a RedisException object on connectivity error, as described above. + * @throws RedisException + * @link https://redis.io/commands/ping + */ + public function ping() + { + } + + /** + * Echo the given string + * + * @param string $message + * + * @return string Returns message + * + * @link https://redis.io/commands/echo + */ + public function echo($message) + { + } + + /** + * Get the value related to the specified key + * + * @param string $key + * + * @return string|mixed|bool If key didn't exist, FALSE is returned. + * Otherwise, the value related to this key is returned + * + * @link https://redis.io/commands/get + * @example + *
+ * $redis->set('key', 'hello');
+ * $redis->get('key');
+ *
+ * // set and get with serializer
+ * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);
+ *
+ * $redis->set('key', ['asd' => 'as', 'dd' => 123, 'b' => true]);
+ * var_dump($redis->get('key'));
+ * // Output:
+ * array(3) {
+ * 'asd' => string(2) "as"
+ * 'dd' => int(123)
+ * 'b' => bool(true)
+ * }
+ *
+ */
+ public function get($key)
+ {
+ }
+
+ /**
+ * Set the string value in argument as value of the key.
+ *
+ * @since If you're using Redis >= 2.6.12, you can pass extended options as explained in example
+ *
+ * @param string $key
+ * @param string|mixed $value string if not used serializer
+ * @param int|array $timeout [optional] Calling setex() is preferred if you want a timeout.
+ * // Simple key -> value set
+ * $redis->set('key', 'value');
+ *
+ * // Will redirect, and actually make an SETEX call
+ * $redis->set('key','value', 10);
+ *
+ * // Will set the key, if it doesn't exist, with a ttl of 10 seconds
+ * $redis->set('key', 'value', ['nx', 'ex' => 10]);
+ *
+ * // Will set a key, if it does exist, with a ttl of 1000 miliseconds
+ * $redis->set('key', 'value', ['xx', 'px' => 1000]);
+ *
+ *
+ * @return bool TRUE if the command is successful
+ *
+ * @link https://redis.io/commands/set
+ */
+ public function set($key, $value, $timeout = null)
+ {
+ }
+
+ /**
+ * Set the string value in argument as value of the key, with a time to live.
+ *
+ * @param string $key
+ * @param int $ttl
+ * @param string|mixed $value
+ *
+ * @return bool TRUE if the command is successful
+ *
+ * @link https://redis.io/commands/setex
+ * @example $redis->setex('key', 3600, 'value'); // sets key → value, with 1h TTL.
+ */
+ public function setex($key, $ttl, $value)
+ {
+ }
+
+ /**
+ * Set the value and expiration in milliseconds of a key.
+ *
+ * @see setex()
+ * @param string $key
+ * @param int $ttl, in milliseconds.
+ * @param string|mixed $value
+ *
+ * @return bool TRUE if the command is successful
+ *
+ * @link https://redis.io/commands/psetex
+ * @example $redis->psetex('key', 1000, 'value'); // sets key → value, with 1sec TTL.
+ */
+ public function psetex($key, $ttl, $value)
+ {
+ }
+
+ /**
+ * Set the string value in argument as value of the key if the key doesn't already exist in the database.
+ *
+ * @param string $key
+ * @param string|mixed $value
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/setnx
+ * @example
+ *
+ * $redis->setnx('key', 'value'); // return TRUE
+ * $redis->setnx('key', 'value'); // return FALSE
+ *
+ */
+ public function setnx($key, $value)
+ {
+ }
+
+ /**
+ * Remove specified keys.
+ *
+ * @param int|string|array $key1 An array of keys, or an undefined number of parameters, each a key: key1 key2 key3 ... keyN
+ * @param int|string ...$otherKeys
+ *
+ * @return int Number of keys deleted
+ *
+ * @link https://redis.io/commands/del
+ * @example
+ *
+ * $redis->set('key1', 'val1');
+ * $redis->set('key2', 'val2');
+ * $redis->set('key3', 'val3');
+ * $redis->set('key4', 'val4');
+ *
+ * $redis->del('key1', 'key2'); // return 2
+ * $redis->del(['key3', 'key4']); // return 2
+ *
+ */
+ public function del($key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * @see del()
+ * @deprecated use Redis::del()
+ *
+ * @param string|string[] $key1
+ * @param string $key2
+ * @param string $key3
+ *
+ * @return int Number of keys deleted
+ */
+ public function delete($key1, $key2 = null, $key3 = null)
+ {
+ }
+
+ /**
+ * Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.
+ *
+ * @see del()
+ * @param string|string[] $key1
+ * @param string $key2
+ * @param string $key3
+ *
+ * @return int Number of keys unlinked.
+ *
+ * @link https://redis.io/commands/unlink
+ * @example
+ *
+ * $redis->set('key1', 'val1');
+ * $redis->set('key2', 'val2');
+ * $redis->set('key3', 'val3');
+ * $redis->set('key4', 'val4');
+ * $redis->unlink('key1', 'key2'); // return 2
+ * $redis->unlink(array('key3', 'key4')); // return 2
+ *
+ */
+ public function unlink($key1, $key2 = null, $key3 = null)
+ {
+ }
+
+ /**
+ * Enter and exit transactional mode.
+ *
+ * @param int $mode Redis::MULTI|Redis::PIPELINE
+ * Defaults to Redis::MULTI.
+ * A Redis::MULTI block of commands runs as a single transaction;
+ * a Redis::PIPELINE block is simply transmitted faster to the server, but without any guarantee of atomicity.
+ * discard cancels a transaction.
+ *
+ * @return Redis returns the Redis instance and enters multi-mode.
+ * Once in multi-mode, all subsequent method calls return the same object until exec() is called.
+ *
+ * @link https://redis.io/commands/multi
+ * @example
+ *
+ * $ret = $redis->multi()
+ * ->set('key1', 'val1')
+ * ->get('key1')
+ * ->set('key2', 'val2')
+ * ->get('key2')
+ * ->exec();
+ *
+ * //$ret == array (
+ * // 0 => TRUE,
+ * // 1 => 'val1',
+ * // 2 => TRUE,
+ * // 3 => 'val2');
+ *
+ */
+ public function multi($mode = Redis::MULTI)
+ {
+ }
+
+ /**
+ * @return void|array
+ *
+ * @see multi()
+ * @link https://redis.io/commands/exec
+ */
+ public function exec()
+ {
+ }
+
+ /**
+ * @see multi()
+ * @link https://redis.io/commands/discard
+ */
+ public function discard()
+ {
+ }
+
+ /**
+ * Watches a key for modifications by another client. If the key is modified between WATCH and EXEC,
+ * the MULTI/EXEC transaction will fail (return FALSE). unwatch cancels all the watching of all keys by this client.
+ * @param string|string[] $key a list of keys
+ *
+ * @return void
+ *
+ * @link https://redis.io/commands/watch
+ * @example
+ *
+ * $redis->watch('x');
+ * // long code here during the execution of which other clients could well modify `x`
+ * $ret = $redis->multi()
+ * ->incr('x')
+ * ->exec();
+ * // $ret = FALSE if x has been modified between the call to WATCH and the call to EXEC.
+ *
+ */
+ public function watch($key)
+ {
+ }
+
+ /**
+ * @see watch()
+ * @link https://redis.io/commands/unwatch
+ */
+ public function unwatch()
+ {
+ }
+
+ /**
+ * Subscribe to channels.
+ *
+ * Warning: this function will probably change in the future.
+ *
+ * @param string[] $channels an array of channels to subscribe
+ * @param string|array $callback either a string or an array($instance, 'method_name').
+ * The callback function receives 3 parameters: the redis instance, the channel name, and the message.
+ *
+ * @return mixed|null Any non-null return value in the callback will be returned to the caller.
+ *
+ * @link https://redis.io/commands/subscribe
+ * @example
+ *
+ * function f($redis, $chan, $msg) {
+ * switch($chan) {
+ * case 'chan-1':
+ * ...
+ * break;
+ *
+ * case 'chan-2':
+ * ...
+ * break;
+ *
+ * case 'chan-2':
+ * ...
+ * break;
+ * }
+ * }
+ *
+ * $redis->subscribe(array('chan-1', 'chan-2', 'chan-3'), 'f'); // subscribe to 3 chans
+ *
+ */
+ public function subscribe($channels, $callback)
+ {
+ }
+
+ /**
+ * Subscribe to channels by pattern
+ *
+ * @param array $patterns an array of glob-style patterns to subscribe
+ * @param string|array $callback Either a string or an array with an object and method.
+ * The callback will get four arguments ($redis, $pattern, $channel, $message)
+ * @param mixed $chan Any non-null return value in the callback will be returned to the caller
+ * @param string $msg
+ *
+ * @link https://redis.io/commands/psubscribe
+ * @example
+ *
+ * function psubscribe($redis, $pattern, $chan, $msg) {
+ * echo "Pattern: $pattern\n";
+ * echo "Channel: $chan\n";
+ * echo "Payload: $msg\n";
+ * }
+ *
+ */
+ public function psubscribe($patterns, $callback, $chan, $msg)
+ {
+ }
+
+ /**
+ * Publish messages to channels.
+ *
+ * Warning: this function will probably change in the future.
+ *
+ * @param string $channel a channel to publish to
+ * @param string $message string
+ *
+ * @return int Number of clients that received the message
+ *
+ * @link https://redis.io/commands/publish
+ * @example $redis->publish('chan-1', 'hello, world!'); // send message.
+ */
+ public function publish($channel, $message)
+ {
+ }
+
+ /**
+ * A command allowing you to get information on the Redis pub/sub system
+ *
+ * @param string $keyword String, which can be: "channels", "numsub", or "numpat"
+ * @param string|array $argument Optional, variant.
+ * For the "channels" subcommand, you can pass a string pattern.
+ * For "numsub" an array of channel names
+ *
+ * @return array|int Either an integer or an array.
+ * - channels Returns an array where the members are the matching channels.
+ * - numsub Returns a key/value array where the keys are channel names and
+ * values are their counts.
+ * - numpat Integer return containing the number active pattern subscriptions
+ *
+ * @link https://redis.io/commands/pubsub
+ * @example
+ *
+ * $redis->pubsub('channels'); // All channels
+ * $redis->pubsub('channels', '*pattern*'); // Just channels matching your pattern
+ * $redis->pubsub('numsub', array('chan1', 'chan2')); // Get subscriber counts for 'chan1' and 'chan2'
+ * $redis->pubsub('numpat'); // Get the number of pattern subscribers
+ *
+ */
+ public function pubsub($keyword, $argument)
+ {
+ }
+
+ /**
+ * Stop listening for messages posted to the given channels.
+ *
+ * @param array $channels an array of channels to usubscribe
+ *
+ * @link https://redis.io/commands/unsubscribe
+ */
+ public function unsubscribe($channels = null)
+ {
+ }
+
+ /**
+ * Stop listening for messages posted to the given channels.
+ *
+ * @param array $patterns an array of glob-style patterns to unsubscribe
+ *
+ * @link https://redis.io/commands/punsubscribe
+ */
+ public function punsubscribe($patterns = null)
+ {
+ }
+
+ /**
+ * Verify if the specified key/keys exists
+ *
+ * This function took a single argument and returned TRUE or FALSE in phpredis versions < 4.0.0.
+ *
+ * @since >= 4.0 Returned int, if < 4.0 returned bool
+ *
+ * @param string|string[] $key
+ *
+ * @return int|bool The number of keys tested that do exist
+ *
+ * @link https://redis.io/commands/exists
+ * @link https://github.com/phpredis/phpredis#exists
+ * @example
+ *
+ * $redis->exists('key'); // 1
+ * $redis->exists('NonExistingKey'); // 0
+ *
+ * $redis->mset(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']);
+ * $redis->exists(['foo', 'bar', 'baz]); // 3
+ * $redis->exists('foo', 'bar', 'baz'); // 3
+ *
+ */
+ public function exists($key)
+ {
+ }
+
+ /**
+ * Increment the number stored at key by one.
+ *
+ * @param string $key
+ *
+ * @return int the new value
+ *
+ * @link https://redis.io/commands/incr
+ * @example
+ *
+ * $redis->incr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value 1
+ * $redis->incr('key1'); // 2
+ * $redis->incr('key1'); // 3
+ * $redis->incr('key1'); // 4
+ *
+ */
+ public function incr($key)
+ {
+ }
+
+ /**
+ * Increment the float value of a key by the given amount
+ *
+ * @param string $key
+ * @param float $increment
+ *
+ * @return float
+ *
+ * @link https://redis.io/commands/incrbyfloat
+ * @example
+ *
+ * $redis->set('x', 3);
+ * $redis->incrByFloat('x', 1.5); // float(4.5)
+ * $redis->get('x'); // float(4.5)
+ *
+ */
+ public function incrByFloat($key, $increment)
+ {
+ }
+
+ /**
+ * Increment the number stored at key by one.
+ * If the second argument is filled, it will be used as the integer value of the increment.
+ *
+ * @param string $key key
+ * @param int $value value that will be added to key (only for incrBy)
+ *
+ * @return int the new value
+ *
+ * @link https://redis.io/commands/incrby
+ * @example
+ *
+ * $redis->incr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value 1
+ * $redis->incr('key1'); // 2
+ * $redis->incr('key1'); // 3
+ * $redis->incr('key1'); // 4
+ * $redis->incrBy('key1', 10); // 14
+ *
+ */
+ public function incrBy($key, $value)
+ {
+ }
+
+ /**
+ * Decrement the number stored at key by one.
+ *
+ * @param string $key
+ *
+ * @return int the new value
+ *
+ * @link https://redis.io/commands/decr
+ * @example
+ *
+ * $redis->decr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value -1
+ * $redis->decr('key1'); // -2
+ * $redis->decr('key1'); // -3
+ *
+ */
+ public function decr($key)
+ {
+ }
+
+ /**
+ * Decrement the number stored at key by one.
+ * If the second argument is filled, it will be used as the integer value of the decrement.
+ *
+ * @param string $key
+ * @param int $value that will be substracted to key (only for decrBy)
+ *
+ * @return int the new value
+ *
+ * @link https://redis.io/commands/decrby
+ * @example
+ *
+ * $redis->decr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value -1
+ * $redis->decr('key1'); // -2
+ * $redis->decr('key1'); // -3
+ * $redis->decrBy('key1', 10); // -13
+ *
+ */
+ public function decrBy($key, $value)
+ {
+ }
+
+ /**
+ * Adds the string values to the head (left) of the list.
+ * Creates the list if the key didn't exist.
+ * If the key exists and is not a list, FALSE is returned.
+ *
+ * @param string $key
+ * @param string|mixed $value1... Variadic list of values to push in key, if dont used serialized, used string
+ *
+ * @return int|bool The new length of the list in case of success, FALSE in case of Failure
+ *
+ * @link https://redis.io/commands/lpush
+ * @example
+ *
+ * $redis->lPush('l', 'v1', 'v2', 'v3', 'v4') // int(4)
+ * var_dump( $redis->lRange('l', 0, -1) );
+ * // Output:
+ * // array(4) {
+ * // [0]=> string(2) "v4"
+ * // [1]=> string(2) "v3"
+ * // [2]=> string(2) "v2"
+ * // [3]=> string(2) "v1"
+ * // }
+ *
+ */
+ public function lPush($key, ...$value1)
+ {
+ }
+
+ /**
+ * Adds the string values to the tail (right) of the list.
+ * Creates the list if the key didn't exist.
+ * If the key exists and is not a list, FALSE is returned.
+ *
+ * @param string $key
+ * @param string|mixed $value1... Variadic list of values to push in key, if dont used serialized, used string
+ *
+ * @return int|bool The new length of the list in case of success, FALSE in case of Failure
+ *
+ * @link https://redis.io/commands/rpush
+ * @example
+ *
+ * $redis->rPush('l', 'v1', 'v2', 'v3', 'v4'); // int(4)
+ * var_dump( $redis->lRange('l', 0, -1) );
+ * // Output:
+ * // array(4) {
+ * // [0]=> string(2) "v1"
+ * // [1]=> string(2) "v2"
+ * // [2]=> string(2) "v3"
+ * // [3]=> string(2) "v4"
+ * // }
+ *
+ */
+ public function rPush($key, ...$value1)
+ {
+ }
+
+ /**
+ * Adds the string value to the head (left) of the list if the list exists.
+ *
+ * @param string $key
+ * @param string|mixed $value String, value to push in key
+ *
+ * @return int|bool The new length of the list in case of success, FALSE in case of Failure.
+ *
+ * @link https://redis.io/commands/lpushx
+ * @example
+ *
+ * $redis->del('key1');
+ * $redis->lPushx('key1', 'A'); // returns 0
+ * $redis->lPush('key1', 'A'); // returns 1
+ * $redis->lPushx('key1', 'B'); // returns 2
+ * $redis->lPushx('key1', 'C'); // returns 3
+ * // key1 now points to the following list: [ 'A', 'B', 'C' ]
+ *
+ */
+ public function lPushx($key, $value)
+ {
+ }
+
+ /**
+ * Adds the string value to the tail (right) of the list if the ist exists. FALSE in case of Failure.
+ *
+ * @param string $key
+ * @param string|mixed $value String, value to push in key
+ *
+ * @return int|bool The new length of the list in case of success, FALSE in case of Failure.
+ *
+ * @link https://redis.io/commands/rpushx
+ * @example
+ *
+ * $redis->del('key1');
+ * $redis->rPushx('key1', 'A'); // returns 0
+ * $redis->rPush('key1', 'A'); // returns 1
+ * $redis->rPushx('key1', 'B'); // returns 2
+ * $redis->rPushx('key1', 'C'); // returns 3
+ * // key1 now points to the following list: [ 'A', 'B', 'C' ]
+ *
+ */
+ public function rPushx($key, $value)
+ {
+ }
+
+ /**
+ * Returns and removes the first element of the list.
+ *
+ * @param string $key
+ *
+ * @return mixed|bool if command executed successfully BOOL FALSE in case of failure (empty list)
+ *
+ * @link https://redis.io/commands/lpop
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
+ * $redis->lPop('key1'); // key1 => [ 'B', 'C' ]
+ *
+ */
+ public function lPop($key)
+ {
+ }
+
+ /**
+ * Returns and removes the last element of the list.
+ *
+ * @param string $key
+ *
+ * @return mixed|bool if command executed successfully BOOL FALSE in case of failure (empty list)
+ *
+ * @link https://redis.io/commands/rpop
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
+ * $redis->rPop('key1'); // key1 => [ 'A', 'B' ]
+ *
+ */
+ public function rPop($key)
+ {
+ }
+
+ /**
+ * Is a blocking lPop primitive. If at least one of the lists contains at least one element,
+ * the element will be popped from the head of the list and returned to the caller.
+ * Il all the list identified by the keys passed in arguments are empty, blPop will block
+ * during the specified timeout until an element is pushed to one of those lists. This element will be popped.
+ *
+ * @param string|string[] $keys String array containing the keys of the lists OR variadic list of strings
+ * @param int $timeout Timeout is always the required final parameter
+ *
+ * @return array ['listName', 'element']
+ *
+ * @link https://redis.io/commands/blpop
+ * @example
+ *
+ * // Non blocking feature
+ * $redis->lPush('key1', 'A');
+ * $redis->del('key2');
+ *
+ * $redis->blPop('key1', 'key2', 10); // array('key1', 'A')
+ * // OR
+ * $redis->blPop(['key1', 'key2'], 10); // array('key1', 'A')
+ *
+ * $redis->brPop('key1', 'key2', 10); // array('key1', 'A')
+ * // OR
+ * $redis->brPop(['key1', 'key2'], 10); // array('key1', 'A')
+ *
+ * // Blocking feature
+ *
+ * // process 1
+ * $redis->del('key1');
+ * $redis->blPop('key1', 10);
+ * // blocking for 10 seconds
+ *
+ * // process 2
+ * $redis->lPush('key1', 'A');
+ *
+ * // process 1
+ * // array('key1', 'A') is returned
+ *
+ */
+ public function blPop($keys, $timeout)
+ {
+ }
+
+ /**
+ * Is a blocking rPop primitive. If at least one of the lists contains at least one element,
+ * the element will be popped from the head of the list and returned to the caller.
+ * Il all the list identified by the keys passed in arguments are empty, brPop will
+ * block during the specified timeout until an element is pushed to one of those lists. T
+ * his element will be popped.
+ *
+ * @param string|string[] $keys String array containing the keys of the lists OR variadic list of strings
+ * @param int $timeout Timeout is always the required final parameter
+ *
+ * @return array ['listName', 'element']
+ *
+ * @link https://redis.io/commands/brpop
+ * @example
+ *
+ * // Non blocking feature
+ * $redis->lPush('key1', 'A');
+ * $redis->del('key2');
+ *
+ * $redis->blPop('key1', 'key2', 10); // array('key1', 'A')
+ * // OR
+ * $redis->blPop(array('key1', 'key2'), 10); // array('key1', 'A')
+ *
+ * $redis->brPop('key1', 'key2', 10); // array('key1', 'A')
+ * // OR
+ * $redis->brPop(array('key1', 'key2'), 10); // array('key1', 'A')
+ *
+ * // Blocking feature
+ *
+ * // process 1
+ * $redis->del('key1');
+ * $redis->blPop('key1', 10);
+ * // blocking for 10 seconds
+ *
+ * // process 2
+ * $redis->lPush('key1', 'A');
+ *
+ * // process 1
+ * // array('key1', 'A') is returned
+ *
+ */
+ public function brPop(array $keys, $timeout)
+ {
+ }
+
+ /**
+ * Returns the size of a list identified by Key. If the list didn't exist or is empty,
+ * the command returns 0. If the data type identified by Key is not a list, the command return FALSE.
+ *
+ * @param string $key
+ *
+ * @return int|bool The size of the list identified by Key exists.
+ * bool FALSE if the data type identified by Key is not list
+ *
+ * @link https://redis.io/commands/llen
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
+ * $redis->lLen('key1'); // 3
+ * $redis->rPop('key1');
+ * $redis->lLen('key1'); // 2
+ *
+ */
+ public function lLen($key)
+ {
+ }
+
+ /**
+ * @see lLen()
+ * @link https://redis.io/commands/llen
+ * @deprecated use Redis::lLen()
+ *
+ * @param string $key
+ *
+ * @return int The size of the list identified by Key exists
+ */
+ public function lSize($key)
+ {
+ }
+
+ /**
+ * Return the specified element of the list stored at the specified key.
+ * 0 the first element, 1 the second ... -1 the last element, -2 the penultimate ...
+ * Return FALSE in case of a bad index or a key that doesn't point to a list.
+ *
+ * @param string $key
+ * @param int $index
+ *
+ * @return mixed|bool the element at this index
+ *
+ * Bool FALSE if the key identifies a non-string data type, or no value corresponds to this index in the list Key.
+ *
+ * @link https://redis.io/commands/lindex
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
+ * $redis->lIndex('key1', 0); // 'A'
+ * $redis->lIndex('key1', -1); // 'C'
+ * $redis->lIndex('key1', 10); // `FALSE`
+ *
+ */
+ public function lIndex($key, $index)
+ {
+ }
+
+ /**
+ * @see lIndex()
+ * @link https://redis.io/commands/lindex
+ * @deprecated use Redis::lIndex()
+ *
+ * @param string $key
+ * @param int $index
+ * @return mixed|bool the element at this index
+ */
+ public function lGet($key, $index)
+ {
+ }
+
+ /**
+ * Set the list at index with the new value.
+ *
+ * @param string $key
+ * @param int $index
+ * @param string $value
+ *
+ * @return bool TRUE if the new value is setted.
+ * FALSE if the index is out of range, or data type identified by key is not a list.
+ *
+ * @link https://redis.io/commands/lset
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
+ * $redis->lIndex('key1', 0); // 'A'
+ * $redis->lSet('key1', 0, 'X');
+ * $redis->lIndex('key1', 0); // 'X'
+ *
+ */
+ public function lSet($key, $index, $value)
+ {
+ }
+
+ /**
+ * Returns the specified elements of the list stored at the specified key in
+ * the range [start, end]. start and stop are interpretated as indices: 0 the first element,
+ * 1 the second ... -1 the last element, -2 the penultimate ...
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ *
+ * @return array containing the values in specified range.
+ *
+ * @link https://redis.io/commands/lrange
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C');
+ * $redis->lRange('key1', 0, -1); // array('A', 'B', 'C')
+ *
+ */
+ public function lRange($key, $start, $end)
+ {
+ }
+
+ /**
+ * @see lRange()
+ * @link https://redis.io/commands/lrange
+ * @deprecated use Redis::lRange()
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ * @return array
+ */
+ public function lGetRange($key, $start, $end)
+ {
+ }
+
+ /**
+ * Trims an existing list so that it will contain only a specified range of elements.
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $stop
+ *
+ * @return array|bool Bool return FALSE if the key identify a non-list value
+ *
+ * @link https://redis.io/commands/ltrim
+ * @example
+ *
+ * $redis->rPush('key1', 'A');
+ * $redis->rPush('key1', 'B');
+ * $redis->rPush('key1', 'C');
+ * $redis->lRange('key1', 0, -1); // array('A', 'B', 'C')
+ * $redis->lTrim('key1', 0, 1);
+ * $redis->lRange('key1', 0, -1); // array('A', 'B')
+ *
+ */
+ public function lTrim($key, $start, $stop)
+ {
+ }
+
+ /**
+ * @see lTrim()
+ * @link https://redis.io/commands/ltrim
+ * @deprecated use Redis::lTrim()
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $stop
+ */
+ public function listTrim($key, $start, $stop)
+ {
+ }
+
+ /**
+ * Removes the first count occurences of the value element from the list.
+ * If count is zero, all the matching elements are removed. If count is negative,
+ * elements are removed from tail to head.
+ *
+ * @param string $key
+ * @param string $value
+ * @param int $count
+ *
+ * @return int|bool the number of elements to remove
+ * bool FALSE if the value identified by key is not a list.
+ *
+ * @link https://redis.io/commands/lrem
+ * @example
+ *
+ * $redis->lPush('key1', 'A');
+ * $redis->lPush('key1', 'B');
+ * $redis->lPush('key1', 'C');
+ * $redis->lPush('key1', 'A');
+ * $redis->lPush('key1', 'A');
+ *
+ * $redis->lRange('key1', 0, -1); // array('A', 'A', 'C', 'B', 'A')
+ * $redis->lRem('key1', 'A', 2); // 2
+ * $redis->lRange('key1', 0, -1); // array('C', 'B', 'A')
+ *
+ */
+ public function lRem($key, $value, $count)
+ {
+ }
+
+ /**
+ * @see lRem
+ * @link https://redis.io/commands/lremove
+ * @deprecated use Redis::lRem()
+ *
+ * @param string $key
+ * @param string $value
+ * @param int $count
+ */
+ public function lRemove($key, $value, $count)
+ {
+ }
+
+ /**
+ * Insert value in the list before or after the pivot value. the parameter options
+ * specify the position of the insert (before or after). If the list didn't exists,
+ * or the pivot didn't exists, the value is not inserted.
+ *
+ * @param string $key
+ * @param int $position Redis::BEFORE | Redis::AFTER
+ * @param string $pivot
+ * @param string|mixed $value
+ *
+ * @return int The number of the elements in the list, -1 if the pivot didn't exists.
+ *
+ * @link https://redis.io/commands/linsert
+ * @example
+ *
+ * $redis->del('key1');
+ * $redis->lInsert('key1', Redis::AFTER, 'A', 'X'); // 0
+ *
+ * $redis->lPush('key1', 'A');
+ * $redis->lPush('key1', 'B');
+ * $redis->lPush('key1', 'C');
+ *
+ * $redis->lInsert('key1', Redis::BEFORE, 'C', 'X'); // 4
+ * $redis->lRange('key1', 0, -1); // array('A', 'B', 'X', 'C')
+ *
+ * $redis->lInsert('key1', Redis::AFTER, 'C', 'Y'); // 5
+ * $redis->lRange('key1', 0, -1); // array('A', 'B', 'X', 'C', 'Y')
+ *
+ * $redis->lInsert('key1', Redis::AFTER, 'W', 'value'); // -1
+ *
+ */
+ public function lInsert($key, $position, $pivot, $value)
+ {
+ }
+
+ /**
+ * Adds a values to the set value stored at key.
+ *
+ * @param string $key Required key
+ * @param string|mixed ...$value1 Variadic list of values
+ *
+ * @return int|bool The number of elements added to the set.
+ * If this value is already in the set, FALSE is returned
+ *
+ * @link https://redis.io/commands/sadd
+ * @example
+ *
+ * $redis->sAdd('k', 'v1'); // int(1)
+ * $redis->sAdd('k', 'v1', 'v2', 'v3'); // int(2)
+ *
+ */
+ public function sAdd($key, ...$value1)
+ {
+ }
+
+ /**
+ * Removes the specified members from the set value stored at key.
+ *
+ * @param string $key
+ * @param string|mixed ...$member1 Variadic list of members
+ *
+ * @return int The number of elements removed from the set
+ *
+ * @link https://redis.io/commands/srem
+ * @example
+ *
+ * var_dump( $redis->sAdd('k', 'v1', 'v2', 'v3') ); // int(3)
+ * var_dump( $redis->sRem('k', 'v2', 'v3') ); // int(2)
+ * var_dump( $redis->sMembers('k') );
+ * //// Output:
+ * // array(1) {
+ * // [0]=> string(2) "v1"
+ * // }
+ *
+ */
+ public function sRem($key, ...$member1)
+ {
+ }
+
+ /**
+ * @see sRem()
+ * @link https://redis.io/commands/srem
+ * @deprecated use Redis::sRem()
+ *
+ * @param string $key
+ * @param string|mixed ...$member1
+ */
+ public function sRemove($key, ...$member1)
+ {
+ }
+
+ /**
+ * Moves the specified member from the set at srcKey to the set at dstKey.
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ * @param string|mixed $member
+ *
+ * @return bool If the operation is successful, return TRUE.
+ * If the srcKey and/or dstKey didn't exist, and/or the member didn't exist in srcKey, FALSE is returned.
+ *
+ * @link https://redis.io/commands/smove
+ * @example
+ *
+ * $redis->sAdd('key1' , 'set11');
+ * $redis->sAdd('key1' , 'set12');
+ * $redis->sAdd('key1' , 'set13'); // 'key1' => {'set11', 'set12', 'set13'}
+ * $redis->sAdd('key2' , 'set21');
+ * $redis->sAdd('key2' , 'set22'); // 'key2' => {'set21', 'set22'}
+ * $redis->sMove('key1', 'key2', 'set13'); // 'key1' => {'set11', 'set12'}
+ * // 'key2' => {'set21', 'set22', 'set13'}
+ *
+ */
+ public function sMove($srcKey, $dstKey, $member)
+ {
+ }
+
+ /**
+ * Checks if value is a member of the set stored at the key key.
+ *
+ * @param string $key
+ * @param string|mixed $value
+ *
+ * @return bool TRUE if value is a member of the set at key key, FALSE otherwise
+ *
+ * @link https://redis.io/commands/sismember
+ * @example
+ *
+ * $redis->sAdd('key1' , 'set1');
+ * $redis->sAdd('key1' , 'set2');
+ * $redis->sAdd('key1' , 'set3'); // 'key1' => {'set1', 'set2', 'set3'}
+ *
+ * $redis->sIsMember('key1', 'set1'); // TRUE
+ * $redis->sIsMember('key1', 'setX'); // FALSE
+ *
+ */
+ public function sIsMember($key, $value)
+ {
+ }
+
+ /**
+ * @see sIsMember()
+ * @link https://redis.io/commands/sismember
+ * @deprecated use Redis::sIsMember()
+ *
+ * @param string $key
+ * @param string|mixed $value
+ */
+ public function sContains($key, $value)
+ {
+ }
+
+ /**
+ * Returns the cardinality of the set identified by key.
+ *
+ * @param string $key
+ *
+ * @return int the cardinality of the set identified by key, 0 if the set doesn't exist.
+ *
+ * @link https://redis.io/commands/scard
+ * @example
+ *
+ * $redis->sAdd('key1' , 'set1');
+ * $redis->sAdd('key1' , 'set2');
+ * $redis->sAdd('key1' , 'set3'); // 'key1' => {'set1', 'set2', 'set3'}
+ * $redis->sCard('key1'); // 3
+ * $redis->sCard('keyX'); // 0
+ *
+ */
+ public function sCard($key)
+ {
+ }
+
+ /**
+ * Removes and returns a random element from the set value at Key.
+ *
+ * @param string $key
+ *
+ * @return string|mixed|bool "popped" value
+ * bool FALSE if set identified by key is empty or doesn't exist.
+ *
+ * @link https://redis.io/commands/spop
+ * @example
+ *
+ * $redis->sAdd('key1' , 'set1');
+ * $redis->sAdd('key1' , 'set2');
+ * $redis->sAdd('key1' , 'set3'); // 'key1' => {'set3', 'set1', 'set2'}
+ * $redis->sPop('key1'); // 'set1', 'key1' => {'set3', 'set2'}
+ * $redis->sPop('key1'); // 'set3', 'key1' => {'set2'}
+ *
+ */
+ public function sPop($key)
+ {
+ }
+
+ /**
+ * Returns a random element(s) from the set value at Key, without removing it.
+ *
+ * @param string $key
+ * @param int $count [optional]
+ *
+ * @return string|mixed|array|bool value(s) from the set
+ * bool FALSE if set identified by key is empty or doesn't exist and count argument isn't passed.
+ *
+ * @link https://redis.io/commands/srandmember
+ * @example
+ *
+ * $redis->sAdd('key1' , 'one');
+ * $redis->sAdd('key1' , 'two');
+ * $redis->sAdd('key1' , 'three'); // 'key1' => {'one', 'two', 'three'}
+ *
+ * var_dump( $redis->sRandMember('key1') ); // 'key1' => {'one', 'two', 'three'}
+ *
+ * // string(5) "three"
+ *
+ * var_dump( $redis->sRandMember('key1', 2) ); // 'key1' => {'one', 'two', 'three'}
+ *
+ * // array(2) {
+ * // [0]=> string(2) "one"
+ * // [1]=> string(2) "three"
+ * // }
+ *
+ */
+ public function sRandMember($key, $count = 1)
+ {
+ }
+
+ /**
+ * Returns the members of a set resulting from the intersection of all the sets
+ * held at the specified keys. If just a single key is specified, then this command
+ * produces the members of this set. If one of the keys is missing, FALSE is returned.
+ *
+ * @param string $key1 keys identifying the different sets on which we will apply the intersection.
+ * @param string ...$otherKeys variadic list of keys
+ *
+ * @return array contain the result of the intersection between those keys
+ * If the intersection between the different sets is empty, the return value will be empty array.
+ *
+ * @link https://redis.io/commands/sinter
+ * @example
+ *
+ * $redis->sAdd('key1', 'val1');
+ * $redis->sAdd('key1', 'val2');
+ * $redis->sAdd('key1', 'val3');
+ * $redis->sAdd('key1', 'val4');
+ *
+ * $redis->sAdd('key2', 'val3');
+ * $redis->sAdd('key2', 'val4');
+ *
+ * $redis->sAdd('key3', 'val3');
+ * $redis->sAdd('key3', 'val4');
+ *
+ * var_dump($redis->sInter('key1', 'key2', 'key3'));
+ *
+ * //array(2) {
+ * // [0]=>
+ * // string(4) "val4"
+ * // [1]=>
+ * // string(4) "val3"
+ * //}
+ *
+ */
+ public function sInter($key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Performs a sInter command and stores the result in a new set.
+ *
+ * @param string $dstKey the key to store the diff into.
+ * @param string $key1 keys identifying the different sets on which we will apply the intersection.
+ * @param string ...$otherKeys variadic list of keys
+ *
+ * @return int|bool The cardinality of the resulting set, or FALSE in case of a missing key
+ *
+ * @link https://redis.io/commands/sinterstore
+ * @example
+ *
+ * $redis->sAdd('key1', 'val1');
+ * $redis->sAdd('key1', 'val2');
+ * $redis->sAdd('key1', 'val3');
+ * $redis->sAdd('key1', 'val4');
+ *
+ * $redis->sAdd('key2', 'val3');
+ * $redis->sAdd('key2', 'val4');
+ *
+ * $redis->sAdd('key3', 'val3');
+ * $redis->sAdd('key3', 'val4');
+ *
+ * var_dump($redis->sInterStore('output', 'key1', 'key2', 'key3'));
+ * var_dump($redis->sMembers('output'));
+ *
+ * //int(2)
+ * //
+ * //array(2) {
+ * // [0]=>
+ * // string(4) "val4"
+ * // [1]=>
+ * // string(4) "val3"
+ * //}
+ *
+ */
+ public function sInterStore($dstKey, $key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Performs the union between N sets and returns it.
+ *
+ * @param string $key1 first key for union
+ * @param string ...$otherKeys variadic list of keys corresponding to sets in redis
+ *
+ * @return array string[] The union of all these sets
+ *
+ * @link https://redis.io/commands/sunionstore
+ * @example
+ *
+ * $redis->sAdd('s0', '1');
+ * $redis->sAdd('s0', '2');
+ * $redis->sAdd('s1', '3');
+ * $redis->sAdd('s1', '1');
+ * $redis->sAdd('s2', '3');
+ * $redis->sAdd('s2', '4');
+ *
+ * var_dump($redis->sUnion('s0', 's1', 's2'));
+ *
+ * array(4) {
+ * // [0]=>
+ * // string(1) "3"
+ * // [1]=>
+ * // string(1) "4"
+ * // [2]=>
+ * // string(1) "1"
+ * // [3]=>
+ * // string(1) "2"
+ * //}
+ *
+ */
+ public function sUnion($key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Performs the same action as sUnion, but stores the result in the first key
+ *
+ * @param string $dstKey the key to store the diff into.
+ * @param string $key1 first key for union
+ * @param string ...$otherKeys variadic list of keys corresponding to sets in redis
+ *
+ * @return int Any number of keys corresponding to sets in redis
+ *
+ * @link https://redis.io/commands/sunionstore
+ * @example
+ *
+ * $redis->del('s0', 's1', 's2');
+ *
+ * $redis->sAdd('s0', '1');
+ * $redis->sAdd('s0', '2');
+ * $redis->sAdd('s1', '3');
+ * $redis->sAdd('s1', '1');
+ * $redis->sAdd('s2', '3');
+ * $redis->sAdd('s2', '4');
+ *
+ * var_dump($redis->sUnionStore('dst', 's0', 's1', 's2'));
+ * var_dump($redis->sMembers('dst'));
+ *
+ * //int(4)
+ * //array(4) {
+ * // [0]=>
+ * // string(1) "3"
+ * // [1]=>
+ * // string(1) "4"
+ * // [2]=>
+ * // string(1) "1"
+ * // [3]=>
+ * // string(1) "2"
+ * //}
+ *
+ */
+ public function sUnionStore($dstKey, $key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Performs the difference between N sets and returns it.
+ *
+ * @param string $key1 first key for diff
+ * @param string ...$otherKeys variadic list of keys corresponding to sets in redis
+ *
+ * @return array string[] The difference of the first set will all the others
+ *
+ * @link https://redis.io/commands/sdiff
+ * @example
+ *
+ * $redis->del('s0', 's1', 's2');
+ *
+ * $redis->sAdd('s0', '1');
+ * $redis->sAdd('s0', '2');
+ * $redis->sAdd('s0', '3');
+ * $redis->sAdd('s0', '4');
+ *
+ * $redis->sAdd('s1', '1');
+ * $redis->sAdd('s2', '3');
+ *
+ * var_dump($redis->sDiff('s0', 's1', 's2'));
+ *
+ * //array(2) {
+ * // [0]=>
+ * // string(1) "4"
+ * // [1]=>
+ * // string(1) "2"
+ * //}
+ *
+ */
+ public function sDiff($key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Performs the same action as sDiff, but stores the result in the first key
+ *
+ * @param string $dstKey the key to store the diff into.
+ * @param string $key1 first key for diff
+ * @param string ...$otherKeys variadic list of keys corresponding to sets in redis
+ *
+ * @return int|bool The cardinality of the resulting set, or FALSE in case of a missing key
+ *
+ * @link https://redis.io/commands/sdiffstore
+ * @example
+ *
+ * $redis->del('s0', 's1', 's2');
+ *
+ * $redis->sAdd('s0', '1');
+ * $redis->sAdd('s0', '2');
+ * $redis->sAdd('s0', '3');
+ * $redis->sAdd('s0', '4');
+ *
+ * $redis->sAdd('s1', '1');
+ * $redis->sAdd('s2', '3');
+ *
+ * var_dump($redis->sDiffStore('dst', 's0', 's1', 's2'));
+ * var_dump($redis->sMembers('dst'));
+ *
+ * //int(2)
+ * //array(2) {
+ * // [0]=>
+ * // string(1) "4"
+ * // [1]=>
+ * // string(1) "2"
+ * //}
+ *
+ */
+ public function sDiffStore($dstKey, $key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Returns the contents of a set.
+ *
+ * @param string $key
+ *
+ * @return array An array of elements, the contents of the set
+ *
+ * @link https://redis.io/commands/smembers
+ * @example
+ *
+ * $redis->del('s');
+ * $redis->sAdd('s', 'a');
+ * $redis->sAdd('s', 'b');
+ * $redis->sAdd('s', 'a');
+ * $redis->sAdd('s', 'c');
+ * var_dump($redis->sMembers('s'));
+ *
+ * //array(3) {
+ * // [0]=>
+ * // string(1) "c"
+ * // [1]=>
+ * // string(1) "a"
+ * // [2]=>
+ * // string(1) "b"
+ * //}
+ * // The order is random and corresponds to redis' own internal representation of the set structure.
+ *
+ */
+ public function sMembers($key)
+ {
+ }
+
+ /**
+ * @see sMembers()
+ * @link https://redis.io/commands/smembers
+ * @deprecated use Redis::sMembers()
+ *
+ * @param string $key
+ * @return array An array of elements, the contents of the set
+ */
+ public function sGetMembers($key)
+ {
+ }
+
+ /**
+ * Scan a set for members
+ *
+ * @param string $key The set to search.
+ * @param int $iterator LONG (reference) to the iterator as we go.
+ * @param string $pattern String, optional pattern to match against.
+ * @param int $count How many members to return at a time (Redis might return a different amount)
+ *
+ * @return array|bool PHPRedis will return an array of keys or FALSE when we're done iterating
+ *
+ * @link https://redis.io/commands/sscan
+ * @example
+ *
+ * $iterator = null;
+ * while ($members = $redis->sScan('set', $iterator)) {
+ * foreach ($members as $member) {
+ * echo $member . PHP_EOL;
+ * }
+ * }
+ *
+ */
+ public function sScan($key, &$iterator, $pattern = null, $count = 0)
+ {
+ }
+
+ /**
+ * Sets a value and returns the previous entry at that key.
+ *
+ * @param string $key
+ * @param string|mixed $value
+ *
+ * @return string|mixed A string (mixed, if used serializer), the previous value located at this key
+ *
+ * @link https://redis.io/commands/getset
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $exValue = $redis->getSet('x', 'lol'); // return '42', replaces x by 'lol'
+ * $newValue = $redis->get('x')' // return 'lol'
+ *
+ */
+ public function getSet($key, $value)
+ {
+ }
+
+ /**
+ * Returns a random key
+ *
+ * @return string an existing key in redis
+ *
+ * @link https://redis.io/commands/randomkey
+ * @example
+ * + * $key = $redis->randomKey(); + * $surprise = $redis->get($key); // who knows what's in there. + *+ */ + public function randomKey() + { + } + + /** + * Switches to a given database + * + * @param int $dbIndex + * + * @return bool TRUE in case of success, FALSE in case of failure + * + * @link https://redis.io/commands/select + * @example + *
+ * $redis->select(0); // switch to DB 0
+ * $redis->set('x', '42'); // write 42 to x
+ * $redis->move('x', 1); // move to DB 1
+ * $redis->select(1); // switch to DB 1
+ * $redis->get('x'); // will return 42
+ *
+ */
+ public function select($dbIndex)
+ {
+ }
+
+ /**
+ * Moves a key to a different database.
+ *
+ * @param string $key
+ * @param int $dbIndex
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/move
+ * @example
+ *
+ * $redis->select(0); // switch to DB 0
+ * $redis->set('x', '42'); // write 42 to x
+ * $redis->move('x', 1); // move to DB 1
+ * $redis->select(1); // switch to DB 1
+ * $redis->get('x'); // will return 42
+ *
+ */
+ public function move($key, $dbIndex)
+ {
+ }
+
+ /**
+ * Renames a key
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/rename
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $redis->rename('x', 'y');
+ * $redis->get('y'); // → 42
+ * $redis->get('x'); // → `FALSE`
+ *
+ */
+ public function rename($srcKey, $dstKey)
+ {
+ }
+
+ /**
+ * @see rename()
+ * @link https://redis.io/commands/rename
+ * @deprecated use Redis::rename()
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ */
+ public function renameKey($srcKey, $dstKey)
+ {
+ }
+
+ /**
+ * Renames a key
+ *
+ * Same as rename, but will not replace a key if the destination already exists.
+ * This is the same behaviour as setNx.
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/renamenx
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $redis->rename('x', 'y');
+ * $redis->get('y'); // → 42
+ * $redis->get('x'); // → `FALSE`
+ *
+ */
+ public function renameNx($srcKey, $dstKey)
+ {
+ }
+
+ /**
+ * Sets an expiration date (a timeout) on an item
+ *
+ * @param string $key The key that will disappear
+ * @param int $ttl The key's remaining Time To Live, in seconds
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/expire
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $redis->expire('x', 3); // x will disappear in 3 seconds.
+ * sleep(5); // wait 5 seconds
+ * $redis->get('x'); // will return `FALSE`, as 'x' has expired.
+ *
+ */
+ public function expire($key, $ttl)
+ {
+ }
+
+ /**
+ * Sets an expiration date (a timeout in milliseconds) on an item
+ *
+ * @param string $key The key that will disappear.
+ * @param int $ttl The key's remaining Time To Live, in milliseconds
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/pexpire
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $redis->pExpire('x', 11500); // x will disappear in 11500 milliseconds.
+ * $redis->ttl('x'); // 12
+ * $redis->pttl('x'); // 11500
+ *
+ */
+ public function pExpire($key, $ttl)
+ {
+ }
+
+ /**
+ * @see expire()
+ * @link https://redis.io/commands/expire
+ * @deprecated use Redis::expire()
+ *
+ * @param string $key
+ * @param int $ttl
+ * @return bool
+ */
+ public function setTimeout($key, $ttl)
+ {
+ }
+
+ /**
+ * Sets an expiration date (a timestamp) on an item.
+ *
+ * @param string $key The key that will disappear.
+ * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time.
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/expireat
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $now = time(NULL); // current timestamp
+ * $redis->expireAt('x', $now + 3); // x will disappear in 3 seconds.
+ * sleep(5); // wait 5 seconds
+ * $redis->get('x'); // will return `FALSE`, as 'x' has expired.
+ *
+ */
+ public function expireAt($key, $timestamp)
+ {
+ }
+
+ /**
+ * Sets an expiration date (a timestamp) on an item. Requires a timestamp in milliseconds
+ *
+ * @param string $key The key that will disappear
+ * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/pexpireat
+ * @example
+ *
+ * $redis->set('x', '42');
+ * $redis->pExpireAt('x', 1555555555005);
+ * echo $redis->ttl('x'); // 218270121
+ * echo $redis->pttl('x'); // 218270120575
+ *
+ */
+ public function pExpireAt($key, $timestamp)
+ {
+ }
+
+ /**
+ * Returns the keys that match a certain pattern.
+ *
+ * @param string $pattern pattern, using '*' as a wildcard
+ *
+ * @return array string[] The keys that match a certain pattern.
+ *
+ * @link https://redis.io/commands/keys
+ * @example
+ *
+ * $allKeys = $redis->keys('*'); // all keys will match this.
+ * $keyWithUserPrefix = $redis->keys('user*');
+ *
+ */
+ public function keys($pattern)
+ {
+ }
+
+ /**
+ * @see keys()
+ * @deprecated use Redis::keys()
+ *
+ * @param string $pattern
+ * @link https://redis.io/commands/keys
+ */
+ public function getKeys($pattern)
+ {
+ }
+
+ /**
+ * Returns the current database's size
+ *
+ * @return int DB size, in number of keys
+ *
+ * @link https://redis.io/commands/dbsize
+ * @example
+ * + * $count = $redis->dbSize(); + * echo "Redis has $count keys\n"; + *+ */ + public function dbSize() + { + } + + /** + * Authenticate the connection using a password. + * Warning: The password is sent in plain-text over the network. + * + * @param string $password + * + * @return bool TRUE if the connection is authenticated, FALSE otherwise + * + * @link https://redis.io/commands/auth + * @example $redis->auth('foobared'); + */ + public function auth($password) + { + } + + /** + * Starts the background rewrite of AOF (Append-Only File) + * + * @return bool TRUE in case of success, FALSE in case of failure + * + * @link https://redis.io/commands/bgrewriteaof + * @example $redis->bgrewriteaof(); + */ + public function bgrewriteaof() + { + } + + /** + * Changes the slave status + * Either host and port, or no parameter to stop being a slave. + * + * @param string $host [optional] + * @param int $port [optional] + * + * @return bool TRUE in case of success, FALSE in case of failure + * + * @link https://redis.io/commands/slaveof + * @example + *
+ * $redis->slaveof('10.0.1.7', 6379);
+ * // ...
+ * $redis->slaveof();
+ *
+ */
+ public function slaveof($host = '127.0.0.1', $port = 6379)
+ {
+ }
+
+ /**
+ * Access the Redis slowLog
+ *
+ * @param string $operation This can be either GET, LEN, or RESET
+ * @param int|null $length If executing a SLOWLOG GET command, you can pass an optional length.
+ *
+ * @return mixed The return value of SLOWLOG will depend on which operation was performed.
+ * - SLOWLOG GET: Array of slowLog entries, as provided by Redis
+ * - SLOGLOG LEN: Integer, the length of the slowLog
+ * - SLOWLOG RESET: Boolean, depending on success
+ *
+ * @example
+ *
+ * // Get ten slowLog entries
+ * $redis->slowLog('get', 10);
+ * // Get the default number of slowLog entries
+ *
+ * $redis->slowLog('get');
+ * // Reset our slowLog
+ * $redis->slowLog('reset');
+ *
+ * // Retrieve slowLog length
+ * $redis->slowLog('len');
+ *
+ *
+ * @link https://redis.io/commands/slowlog
+ */
+ public function slowLog(string $operation, int $length = null)
+ {
+ }
+
+
+ /**
+ * Describes the object pointed to by a key.
+ * The information to retrieve (string) and the key (string).
+ * Info can be one of the following:
+ * - "encoding"
+ * - "refcount"
+ * - "idletime"
+ *
+ * @param string $string
+ * @param string $key
+ *
+ * @return string|int|bool for "encoding", int for "refcount" and "idletime", FALSE if the key doesn't exist.
+ *
+ * @link https://redis.io/commands/object
+ * @example
+ *
+ * $redis->lPush('l', 'Hello, world!');
+ * $redis->object("encoding", "l"); // → ziplist
+ * $redis->object("refcount", "l"); // → 1
+ * $redis->object("idletime", "l"); // → 400 (in seconds, with a precision of 10 seconds).
+ *
+ */
+ public function object($string = '', $key = '')
+ {
+ }
+
+ /**
+ * Performs a synchronous save.
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ * If a save is already running, this command will fail and return FALSE.
+ *
+ * @link https://redis.io/commands/save
+ * @example $redis->save();
+ */
+ public function save()
+ {
+ }
+
+ /**
+ * Performs a background save.
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ * If a save is already running, this command will fail and return FALSE
+ *
+ * @link https://redis.io/commands/bgsave
+ * @example $redis->bgSave();
+ */
+ public function bgsave()
+ {
+ }
+
+ /**
+ * Returns the timestamp of the last disk save.
+ *
+ * @return int timestamp
+ *
+ * @link https://redis.io/commands/lastsave
+ * @example $redis->lastSave();
+ */
+ public function lastSave()
+ {
+ }
+
+ /**
+ * Blocks the current client until all the previous write commands are successfully transferred and
+ * acknowledged by at least the specified number of slaves.
+ *
+ * @param int $numSlaves Number of slaves that need to acknowledge previous write commands.
+ * @param int $timeout Timeout in milliseconds.
+ *
+ * @return int The command returns the number of slaves reached by all the writes performed in the
+ * context of the current connection
+ *
+ * @link https://redis.io/commands/wait
+ * @example $redis->wait(2, 1000);
+ */
+ public function wait($numSlaves, $timeout)
+ {
+ }
+
+ /**
+ * Returns the type of data pointed by a given key.
+ *
+ * @param string $key
+ *
+ * @return int
+ * Depending on the type of the data pointed by the key,
+ * this method will return the following value:
+ * - string: Redis::REDIS_STRING
+ * - set: Redis::REDIS_SET
+ * - list: Redis::REDIS_LIST
+ * - zset: Redis::REDIS_ZSET
+ * - hash: Redis::REDIS_HASH
+ * - other: Redis::REDIS_NOT_FOUND
+ *
+ * @link https://redis.io/commands/type
+ * @example $redis->type('key');
+ */
+ public function type($key)
+ {
+ }
+
+ /**
+ * Append specified string to the string stored in specified key.
+ *
+ * @param string $key
+ * @param string|mixed $value
+ *
+ * @return int Size of the value after the append
+ *
+ * @link https://redis.io/commands/append
+ * @example
+ *
+ * $redis->set('key', 'value1');
+ * $redis->append('key', 'value2'); // 12
+ * $redis->get('key'); // 'value1value2'
+ *
+ */
+ public function append($key, $value)
+ {
+ }
+
+ /**
+ * Return a substring of a larger string
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ *
+ * @return string the substring
+ *
+ * @link https://redis.io/commands/getrange
+ * @example
+ *
+ * $redis->set('key', 'string value');
+ * $redis->getRange('key', 0, 5); // 'string'
+ * $redis->getRange('key', -5, -1); // 'value'
+ *
+ */
+ public function getRange($key, $start, $end)
+ {
+ }
+
+ /**
+ * Return a substring of a larger string
+ *
+ * @deprecated
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ */
+ public function substr($key, $start, $end)
+ {
+ }
+
+ /**
+ * Changes a substring of a larger string.
+ *
+ * @param string $key
+ * @param int $offset
+ * @param string $value
+ *
+ * @return int the length of the string after it was modified
+ *
+ * @link https://redis.io/commands/setrange
+ * @example
+ *
+ * $redis->set('key', 'Hello world');
+ * $redis->setRange('key', 6, "redis"); // returns 11
+ * $redis->get('key'); // "Hello redis"
+ *
+ */
+ public function setRange($key, $offset, $value)
+ {
+ }
+
+ /**
+ * Get the length of a string value.
+ *
+ * @param string $key
+ * @return int
+ *
+ * @link https://redis.io/commands/strlen
+ * @example
+ *
+ * $redis->set('key', 'value');
+ * $redis->strlen('key'); // 5
+ *
+ */
+ public function strlen($key)
+ {
+ }
+
+ /**
+ * Return the position of the first bit set to 1 or 0 in a string. The position is returned, thinking of the
+ * string as an array of bits from left to right, where the first byte's most significant bit is at position 0,
+ * the second byte's most significant bit is at position 8, and so forth.
+ *
+ * @param string $key
+ * @param int $bit
+ * @param int $start
+ * @param int $end
+ *
+ * @return int The command returns the position of the first bit set to 1 or 0 according to the request.
+ * If we look for set bits (the bit argument is 1) and the string is empty or composed of just
+ * zero bytes, -1 is returned. If we look for clear bits (the bit argument is 0) and the string
+ * only contains bit set to 1, the function returns the first bit not part of the string on the
+ * right. So if the string is three bytes set to the value 0xff the command BITPOS key 0 will
+ * return 24, since up to bit 23 all the bits are 1. Basically, the function considers the right
+ * of the string as padded with zeros if you look for clear bits and specify no range or the
+ * start argument only. However, this behavior changes if you are looking for clear bits and
+ * specify a range with both start and end. If no clear bit is found in the specified range, the
+ * function returns -1 as the user specified a clear range and there are no 0 bits in that range.
+ *
+ * @link https://redis.io/commands/bitpos
+ * @example
+ *
+ * $redis->set('key', '\xff\xff');
+ * $redis->bitpos('key', 1); // int(0)
+ * $redis->bitpos('key', 1, 1); // int(8)
+ * $redis->bitpos('key', 1, 3); // int(-1)
+ * $redis->bitpos('key', 0); // int(16)
+ * $redis->bitpos('key', 0, 1); // int(16)
+ * $redis->bitpos('key', 0, 1, 5); // int(-1)
+ *
+ */
+ public function bitpos($key, $bit, $start = 0, $end = null)
+ {
+ }
+
+ /**
+ * Return a single bit out of a larger string
+ *
+ * @param string $key
+ * @param int $offset
+ *
+ * @return int the bit value (0 or 1)
+ *
+ * @link https://redis.io/commands/getbit
+ * @example
+ *
+ * $redis->set('key', "\x7f"); // this is 0111 1111
+ * $redis->getBit('key', 0); // 0
+ * $redis->getBit('key', 1); // 1
+ *
+ */
+ public function getBit($key, $offset)
+ {
+ }
+
+ /**
+ * Changes a single bit of a string.
+ *
+ * @param string $key
+ * @param int $offset
+ * @param bool|int $value bool or int (1 or 0)
+ *
+ * @return int 0 or 1, the value of the bit before it was set
+ *
+ * @link https://redis.io/commands/setbit
+ * @example
+ *
+ * $redis->set('key', "*"); // ord("*") = 42 = 0x2f = "0010 1010"
+ * $redis->setBit('key', 5, 1); // returns 0
+ * $redis->setBit('key', 7, 1); // returns 0
+ * $redis->get('key'); // chr(0x2f) = "/" = b("0010 1111")
+ *
+ */
+ public function setBit($key, $offset, $value)
+ {
+ }
+
+ /**
+ * Count bits in a string
+ *
+ * @param string $key
+ *
+ * @return int The number of bits set to 1 in the value behind the input key
+ *
+ * @link https://redis.io/commands/bitcount
+ * @example
+ *
+ * $redis->set('bit', '345'); // // 11 0011 0011 0100 0011 0101
+ * var_dump( $redis->bitCount('bit', 0, 0) ); // int(4)
+ * var_dump( $redis->bitCount('bit', 1, 1) ); // int(3)
+ * var_dump( $redis->bitCount('bit', 2, 2) ); // int(4)
+ * var_dump( $redis->bitCount('bit', 0, 2) ); // int(11)
+ *
+ */
+ public function bitCount($key)
+ {
+ }
+
+ /**
+ * Bitwise operation on multiple keys.
+ *
+ * @param string $operation either "AND", "OR", "NOT", "XOR"
+ * @param string $retKey return key
+ * @param string $key1 first key
+ * @param string ...$otherKeys variadic list of keys
+ *
+ * @return int The size of the string stored in the destination key
+ *
+ * @link https://redis.io/commands/bitop
+ * @example
+ *
+ * $redis->set('bit1', '1'); // 11 0001
+ * $redis->set('bit2', '2'); // 11 0010
+ *
+ * $redis->bitOp('AND', 'bit', 'bit1', 'bit2'); // bit = 110000
+ * $redis->bitOp('OR', 'bit', 'bit1', 'bit2'); // bit = 110011
+ * $redis->bitOp('NOT', 'bit', 'bit1', 'bit2'); // bit = 110011
+ * $redis->bitOp('XOR', 'bit', 'bit1', 'bit2'); // bit = 11
+ *
+ */
+ public function bitOp($operation, $retKey, $key1, ...$otherKeys)
+ {
+ }
+
+ /**
+ * Removes all entries from the current database.
+ *
+ * @return bool Always TRUE
+ * @link https://redis.io/commands/flushdb
+ * @example $redis->flushDB();
+ */
+ public function flushDB()
+ {
+ }
+
+ /**
+ * Removes all entries from all databases.
+ *
+ * @return bool Always TRUE
+ *
+ * @link https://redis.io/commands/flushall
+ * @example $redis->flushAll();
+ */
+ public function flushAll()
+ {
+ }
+
+ /**
+ * Sort
+ *
+ * @param string $key
+ * @param array $option array(key => value, ...) - optional, with the following keys and values:
+ * - 'by' => 'some_pattern_*',
+ * - 'limit' => array(0, 1),
+ * - 'get' => 'some_other_pattern_*' or an array of patterns,
+ * - 'sort' => 'asc' or 'desc',
+ * - 'alpha' => TRUE,
+ * - 'store' => 'external-key'
+ *
+ * @return array
+ * An array of values, or a number corresponding to the number of elements stored if that was used
+ *
+ * @link https://redis.io/commands/sort
+ * @example
+ *
+ * $redis->del('s');
+ * $redis->sadd('s', 5);
+ * $redis->sadd('s', 4);
+ * $redis->sadd('s', 2);
+ * $redis->sadd('s', 1);
+ * $redis->sadd('s', 3);
+ *
+ * var_dump($redis->sort('s')); // 1,2,3,4,5
+ * var_dump($redis->sort('s', array('sort' => 'desc'))); // 5,4,3,2,1
+ * var_dump($redis->sort('s', array('sort' => 'desc', 'store' => 'out'))); // (int)5
+ *
+ */
+ public function sort($key, $option = null)
+ {
+ }
+
+ /**
+ * Returns an associative array of strings and integers
+ *
+ * @param string $option Optional. The option to provide redis.
+ * SERVER | CLIENTS | MEMORY | PERSISTENCE | STATS | REPLICATION | CPU | CLASTER | KEYSPACE | COMANDSTATS
+ *
+ * Returns an associative array of strings and integers, with the following keys:
+ * - redis_version
+ * - redis_git_sha1
+ * - redis_git_dirty
+ * - arch_bits
+ * - multiplexing_api
+ * - process_id
+ * - uptime_in_seconds
+ * - uptime_in_days
+ * - lru_clock
+ * - used_cpu_sys
+ * - used_cpu_user
+ * - used_cpu_sys_children
+ * - used_cpu_user_children
+ * - connected_clients
+ * - connected_slaves
+ * - client_longest_output_list
+ * - client_biggest_input_buf
+ * - blocked_clients
+ * - used_memory
+ * - used_memory_human
+ * - used_memory_peak
+ * - used_memory_peak_human
+ * - mem_fragmentation_ratio
+ * - mem_allocator
+ * - loading
+ * - aof_enabled
+ * - changes_since_last_save
+ * - bgsave_in_progress
+ * - last_save_time
+ * - total_connections_received
+ * - total_commands_processed
+ * - expired_keys
+ * - evicted_keys
+ * - keyspace_hits
+ * - keyspace_misses
+ * - hash_max_zipmap_entries
+ * - hash_max_zipmap_value
+ * - pubsub_channels
+ * - pubsub_patterns
+ * - latest_fork_usec
+ * - vm_enabled
+ * - role
+ *
+ * @return string
+ *
+ * @link https://redis.io/commands/info
+ * @example
+ *
+ * $redis->info();
+ *
+ * or
+ *
+ * $redis->info("COMMANDSTATS"); //Information on the commands that have been run (>=2.6 only)
+ * $redis->info("CPU"); // just CPU information from Redis INFO
+ *
+ */
+ public function info($option = null)
+ {
+ }
+
+ /**
+ * Resets the statistics reported by Redis using the INFO command (`info()` function).
+ * These are the counters that are reset:
+ * - Keyspace hits
+ * - Keyspace misses
+ * - Number of commands processed
+ * - Number of connections received
+ * - Number of expired keys
+ *
+ * @return bool `TRUE` in case of success, `FALSE` in case of failure.
+ *
+ * @example $redis->resetStat();
+ * @link https://redis.io/commands/config-resetstat
+ */
+ public function resetStat()
+ {
+ }
+
+ /**
+ * Returns the time to live left for a given key, in seconds. If the key doesn't exist, FALSE is returned.
+ *
+ * @param string $key
+ *
+ * @return int|bool the time left to live in seconds
+ *
+ * @link https://redis.io/commands/ttl
+ * @example
+ *
+ * $redis->setex('key', 123, 'test');
+ * $redis->ttl('key'); // int(123)
+ *
+ */
+ public function ttl($key)
+ {
+ }
+
+ /**
+ * Returns a time to live left for a given key, in milliseconds.
+ *
+ * If the key doesn't exist, FALSE is returned.
+ *
+ * @param string $key
+ *
+ * @return int|bool the time left to live in milliseconds
+ *
+ * @link https://redis.io/commands/pttl
+ * @example
+ *
+ * $redis->setex('key', 123, 'test');
+ * $redis->pttl('key'); // int(122999)
+ *
+ */
+ public function pttl($key)
+ {
+ }
+
+ /**
+ * Remove the expiration timer from a key.
+ *
+ * @param string $key
+ *
+ * @return bool TRUE if a timeout was removed, FALSE if the key didn’t exist or didn’t have an expiration timer.
+ *
+ * @link https://redis.io/commands/persist
+ * @example $redis->persist('key');
+ */
+ public function persist($key)
+ {
+ }
+
+ /**
+ * Sets multiple key-value pairs in one atomic command.
+ * MSETNX only returns TRUE if all the keys were set (see SETNX).
+ *
+ * @param array $array Pairs: array(key => value, ...)
+ *
+ * @return bool TRUE in case of success, FALSE in case of failure
+ *
+ * @link https://redis.io/commands/mset
+ * @example
+ *
+ * $redis->mset(array('key0' => 'value0', 'key1' => 'value1'));
+ * var_dump($redis->get('key0'));
+ * var_dump($redis->get('key1'));
+ * // Output:
+ * // string(6) "value0"
+ * // string(6) "value1"
+ *
+ */
+ public function mset(array $array)
+ {
+ }
+
+ /**
+ * Get the values of all the specified keys.
+ * If one or more keys dont exist, the array will contain FALSE at the position of the key.
+ *
+ * @param array $keys Array containing the list of the keys
+ *
+ * @return array Array containing the values related to keys in argument
+ *
+ * @deprecated use Redis::mGet()
+ * @example
+ *
+ * $redis->set('key1', 'value1');
+ * $redis->set('key2', 'value2');
+ * $redis->set('key3', 'value3');
+ * $redis->getMultiple(array('key1', 'key2', 'key3')); // array('value1', 'value2', 'value3');
+ * $redis->getMultiple(array('key0', 'key1', 'key5')); // array(`FALSE`, 'value2', `FALSE`);
+ *
+ */
+ public function getMultiple(array $keys)
+ {
+ }
+
+ /**
+ * Returns the values of all specified keys.
+ *
+ * For every key that does not hold a string value or does not exist,
+ * the special value false is returned. Because of this, the operation never fails.
+ *
+ * @param array $array
+ *
+ * @return array
+ *
+ * @link https://redis.io/commands/mget
+ * @example
+ *
+ * $redis->del('x', 'y', 'z', 'h'); // remove x y z
+ * $redis->mset(array('x' => 'a', 'y' => 'b', 'z' => 'c'));
+ * $redis->hset('h', 'field', 'value');
+ * var_dump($redis->mget(array('x', 'y', 'z', 'h')));
+ * // Output:
+ * // array(3) {
+ * // [0]=> string(1) "a"
+ * // [1]=> string(1) "b"
+ * // [2]=> string(1) "c"
+ * // [3]=> bool(false)
+ * // }
+ *
+ */
+ public function mget(array $array)
+ {
+ }
+
+ /**
+ * @see mset()
+ * @param array $array
+ * @return int 1 (if the keys were set) or 0 (no key was set)
+ *
+ * @link https://redis.io/commands/msetnx
+ */
+ public function msetnx(array $array)
+ {
+ }
+
+ /**
+ * Pops a value from the tail of a list, and pushes it to the front of another list.
+ * Also return this value.
+ *
+ * @since redis >= 1.1
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ *
+ * @return string|mixed|bool The element that was moved in case of success, FALSE in case of failure.
+ *
+ * @link https://redis.io/commands/rpoplpush
+ * @example
+ *
+ * $redis->del('x', 'y');
+ *
+ * $redis->lPush('x', 'abc');
+ * $redis->lPush('x', 'def');
+ * $redis->lPush('y', '123');
+ * $redis->lPush('y', '456');
+ *
+ * // move the last of x to the front of y.
+ * var_dump($redis->rpoplpush('x', 'y'));
+ * var_dump($redis->lRange('x', 0, -1));
+ * var_dump($redis->lRange('y', 0, -1));
+ *
+ * //Output:
+ * //
+ * //string(3) "abc"
+ * //array(1) {
+ * // [0]=>
+ * // string(3) "def"
+ * //}
+ * //array(3) {
+ * // [0]=>
+ * // string(3) "abc"
+ * // [1]=>
+ * // string(3) "456"
+ * // [2]=>
+ * // string(3) "123"
+ * //}
+ *
+ */
+ public function rpoplpush($srcKey, $dstKey)
+ {
+ }
+
+ /**
+ * A blocking version of rpoplpush, with an integral timeout in the third parameter.
+ *
+ * @param string $srcKey
+ * @param string $dstKey
+ * @param int $timeout
+ *
+ * @return string|mixed|bool The element that was moved in case of success, FALSE in case of timeout
+ *
+ * @link https://redis.io/commands/brpoplpush
+ */
+ public function brpoplpush($srcKey, $dstKey, $timeout)
+ {
+ }
+
+ /**
+ * Adds the specified member with a given score to the sorted set stored at key
+ *
+ * @param string $key Required key
+ * @param array $options Options if needed
+ * @param float $score1 Required score
+ * @param string|mixed $value1 Required value
+ * @param float $score2 Optional score
+ * @param string|mixed $value2 Optional value
+ * @param float $scoreN Optional score
+ * @param string|mixed $valueN Optional value
+ *
+ * @return int Number of values added
+ *
+ * @link https://redis.io/commands/zadd
+ * @example
+ *
+ *
+ * $redis->zAdd('z', 1, 'v1', 2, 'v2', 3, 'v3', 4, 'v4' ); // int(2)
+ * $redis->zRem('z', 'v2', 'v3'); // int(2)
+ * $redis->zAdd('z', ['NX'], 5, 'v5'); // int(1)
+ * $redis->zAdd('z', ['NX'], 6, 'v5'); // int(0)
+ * $redis->zAdd('z', 7, 'v6'); // int(1)
+ * $redis->zAdd('z', 8, 'v6'); // int(0)
+ *
+ * var_dump( $redis->zRange('z', 0, -1) );
+ * // Output:
+ * // array(4) {
+ * // [0]=> string(2) "v1"
+ * // [1]=> string(2) "v4"
+ * // [2]=> string(2) "v5"
+ * // [3]=> string(2) "v8"
+ * // }
+ *
+ * var_dump( $redis->zRange('z', 0, -1, true) );
+ * // Output:
+ * // array(4) {
+ * // ["v1"]=> float(1)
+ * // ["v4"]=> float(4)
+ * // ["v5"]=> float(5)
+ * // ["v6"]=> float(8)
+ *
+ *
+ */
+ public function zAdd($key, $options, $score1, $value1, $score2 = null, $value2 = null, $scoreN = null, $valueN = null)
+ {
+ }
+
+ /**
+ * Returns a range of elements from the ordered set stored at the specified key,
+ * with values in the range [start, end]. start and stop are interpreted as zero-based indices:
+ * 0 the first element,
+ * 1 the second ...
+ * -1 the last element,
+ * -2 the penultimate ...
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ * @param bool $withscores
+ *
+ * @return array Array containing the values in specified range.
+ *
+ * @link https://redis.io/commands/zrange
+ * @example
+ *
+ * $redis->zAdd('key1', 0, 'val0');
+ * $redis->zAdd('key1', 2, 'val2');
+ * $redis->zAdd('key1', 10, 'val10');
+ * $redis->zRange('key1', 0, -1); // array('val0', 'val2', 'val10')
+ * // with scores
+ * $redis->zRange('key1', 0, -1, true); // array('val0' => 0, 'val2' => 2, 'val10' => 10)
+ *
+ */
+ public function zRange($key, $start, $end, $withscores = null)
+ {
+ }
+
+ /**
+ * Deletes a specified member from the ordered set.
+ *
+ * @param string $key
+ * @param string|mixed $member1
+ * @param string|mixed ...$otherMembers
+ *
+ * @return int Number of deleted values
+ *
+ * @link https://redis.io/commands/zrem
+ * @example
+ *
+ * $redis->zAdd('z', 1, 'v1', 2, 'v2', 3, 'v3', 4, 'v4' ); // int(2)
+ * $redis->zRem('z', 'v2', 'v3'); // int(2)
+ * var_dump( $redis->zRange('z', 0, -1) );
+ * //// Output:
+ * // array(2) {
+ * // [0]=> string(2) "v1"
+ * // [1]=> string(2) "v4"
+ * // }
+ *
+ */
+ public function zRem($key, $member1, ...$otherMembers)
+ {
+ }
+
+ /**
+ * @see zRem()
+ * @link https://redis.io/commands/zrem
+ * @deprecated use Redis::zRem()
+ *
+ * @param string $key
+ * @param string|mixed $member1
+ * @param string|mixed ...$otherMembers
+ *
+ * @return int Number of deleted values
+ */
+ public function zDelete($key, $member1, ...$otherMembers)
+ {
+ }
+
+ /**
+ * Returns the elements of the sorted set stored at the specified key in the range [start, end]
+ * in reverse order. start and stop are interpretated as zero-based indices:
+ * 0 the first element,
+ * 1 the second ...
+ * -1 the last element,
+ * -2 the penultimate ...
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ * @param bool $withscore
+ *
+ * @return array Array containing the values in specified range.
+ *
+ * @link https://redis.io/commands/zrevrange
+ * @example
+ *
+ * $redis->zAdd('key', 0, 'val0');
+ * $redis->zAdd('key', 2, 'val2');
+ * $redis->zAdd('key', 10, 'val10');
+ * $redis->zRevRange('key', 0, -1); // array('val10', 'val2', 'val0')
+ *
+ * // with scores
+ * $redis->zRevRange('key', 0, -1, true); // array('val10' => 10, 'val2' => 2, 'val0' => 0)
+ *
+ */
+ public function zRevRange($key, $start, $end, $withscore = null)
+ {
+ }
+
+ /**
+ * Returns the elements of the sorted set stored at the specified key which have scores in the
+ * range [start,end]. Adding a parenthesis before start or end excludes it from the range.
+ * +inf and -inf are also valid limits.
+ *
+ * zRevRangeByScore returns the same items in reverse order, when the start and end parameters are swapped.
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ * @param array $options Two options are available:
+ * - withscores => TRUE,
+ * - and limit => array($offset, $count)
+ *
+ * @return array Array containing the values in specified range.
+ *
+ * @link https://redis.io/commands/zrangebyscore
+ * @example
+ *
+ * $redis->zAdd('key', 0, 'val0');
+ * $redis->zAdd('key', 2, 'val2');
+ * $redis->zAdd('key', 10, 'val10');
+ * $redis->zRangeByScore('key', 0, 3); // array('val0', 'val2')
+ * $redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE); // array('val0' => 0, 'val2' => 2)
+ * $redis->zRangeByScore('key', 0, 3, array('limit' => array(1, 1)); // array('val2')
+ * $redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE, 'limit' => array(1, 1)); // array('val2' => 2)
+ *
+ */
+ public function zRangeByScore($key, $start, $end, array $options = array())
+ {
+ }
+
+ /**
+ * @see zRangeByScore()
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ * @param array $options
+ *
+ * @return array
+ */
+ public function zRevRangeByScore($key, $start, $end, array $options = array())
+ {
+ }
+
+ /**
+ * Returns a lexigraphical range of members in a sorted set, assuming the members have the same score. The
+ * min and max values are required to start with '(' (exclusive), '[' (inclusive), or be exactly the values
+ * '-' (negative inf) or '+' (positive inf). The command must be called with either three *or* five
+ * arguments or will return FALSE.
+ *
+ * @param string $key The ZSET you wish to run against.
+ * @param int $min The minimum alphanumeric value you wish to get.
+ * @param int $max The maximum alphanumeric value you wish to get.
+ * @param int $offset Optional argument if you wish to start somewhere other than the first element.
+ * @param int $limit Optional argument if you wish to limit the number of elements returned.
+ *
+ * @return array|bool Array containing the values in the specified range.
+ *
+ * @link https://redis.io/commands/zrangebylex
+ * @example
+ *
+ * foreach (array('a', 'b', 'c', 'd', 'e', 'f', 'g') as $char) {
+ * $redis->zAdd('key', $char);
+ * }
+ *
+ * $redis->zRangeByLex('key', '-', '[c'); // array('a', 'b', 'c')
+ * $redis->zRangeByLex('key', '-', '(c'); // array('a', 'b')
+ * $redis->zRangeByLex('key', '-', '[c'); // array('b', 'c')
+ *
+ */
+ public function zRangeByLex($key, $min, $max, $offset = null, $limit = null)
+ {
+ }
+
+ /**
+ * @see zRangeByLex()
+ * @param string $key
+ * @param int $min
+ * @param int $max
+ * @param int $offset
+ * @param int $limit
+ *
+ * @return array
+ *
+ * @link https://redis.io/commands/zrevrangebylex
+ */
+ public function zRevRangeByLex($key, $min, $max, $offset = null, $limit = null)
+ {
+ }
+
+ /**
+ * Returns the number of elements of the sorted set stored at the specified key which have
+ * scores in the range [start,end]. Adding a parenthesis before start or end excludes it
+ * from the range. +inf and -inf are also valid limits.
+ *
+ * @param string $key
+ * @param string $start
+ * @param string $end
+ *
+ * @return int the size of a corresponding zRangeByScore
+ *
+ * @link https://redis.io/commands/zcount
+ * @example
+ *
+ * $redis->zAdd('key', 0, 'val0');
+ * $redis->zAdd('key', 2, 'val2');
+ * $redis->zAdd('key', 10, 'val10');
+ * $redis->zCount('key', 0, 3); // 2, corresponding to array('val0', 'val2')
+ *
+ */
+ public function zCount($key, $start, $end)
+ {
+ }
+
+ /**
+ * Deletes the elements of the sorted set stored at the specified key which have scores in the range [start,end].
+ *
+ * @param string $key
+ * @param float|string $start double or "+inf" or "-inf" string
+ * @param float|string $end double or "+inf" or "-inf" string
+ *
+ * @return int The number of values deleted from the sorted set
+ *
+ * @link https://redis.io/commands/zremrangebyscore
+ * @example
+ *
+ * $redis->zAdd('key', 0, 'val0');
+ * $redis->zAdd('key', 2, 'val2');
+ * $redis->zAdd('key', 10, 'val10');
+ * $redis->zRemRangeByScore('key', 0, 3); // 2
+ *
+ */
+ public function zRemRangeByScore($key, $start, $end)
+ {
+ }
+
+ /**
+ * @see zRemRangeByScore()
+ * @deprecated use Redis::zRemRangeByScore()
+ *
+ * @param string $key
+ * @param float $start
+ * @param float $end
+ */
+ public function zDeleteRangeByScore($key, $start, $end)
+ {
+ }
+
+ /**
+ * Deletes the elements of the sorted set stored at the specified key which have rank in the range [start,end].
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ *
+ * @return int The number of values deleted from the sorted set
+ *
+ * @link https://redis.io/commands/zremrangebyrank
+ * @example
+ *
+ * $redis->zAdd('key', 1, 'one');
+ * $redis->zAdd('key', 2, 'two');
+ * $redis->zAdd('key', 3, 'three');
+ * $redis->zRemRangeByRank('key', 0, 1); // 2
+ * $redis->zRange('key', 0, -1, array('withscores' => TRUE)); // array('three' => 3)
+ *
+ */
+ public function zRemRangeByRank($key, $start, $end)
+ {
+ }
+
+ /**
+ * @see zRemRangeByRank()
+ * @link https://redis.io/commands/zremrangebyscore
+ * @deprecated use Redis::zRemRangeByRank()
+ *
+ * @param string $key
+ * @param int $start
+ * @param int $end
+ */
+ public function zDeleteRangeByRank($key, $start, $end)
+ {
+ }
+
+ /**
+ * Returns the cardinality of an ordered set.
+ *
+ * @param string $key
+ *
+ * @return int the set's cardinality
+ *
+ * @link https://redis.io/commands/zsize
+ * @example
+ *
+ * $redis->zAdd('key', 0, 'val0');
+ * $redis->zAdd('key', 2, 'val2');
+ * $redis->zAdd('key', 10, 'val10');
+ * $redis->zCard('key'); // 3
+ *
+ */
+ public function zCard($key)
+ {
+ }
+
+ /**
+ * @see zCard()
+ * @deprecated use Redis::zCard()
+ *
+ * @param string $key
+ * @return int
+ */
+ public function zSize($key)
+ {
+ }
+
+ /**
+ * Returns the score of a given member in the specified sorted set.
+ *
+ * @param string $key
+ * @param string|mixed $member
+ *
+ * @return float|bool false if member or key not exists
+ *
+ * @link https://redis.io/commands/zscore
+ * @example
+ *
+ * $redis->zAdd('key', 2.5, 'val2');
+ * $redis->zScore('key', 'val2'); // 2.5
+ *
+ */
+ public function zScore($key, $member)
+ {
+ }
+
+ /**
+ * Returns the rank of a given member in the specified sorted set, starting at 0 for the item
+ * with the smallest score. zRevRank starts at 0 for the item with the largest score.
+ *
+ * @param string $key
+ * @param string|mixed $member
+ *
+ * @return int|bool the item's score, or false if key or member is not exists
+ *
+ * @link https://redis.io/commands/zrank
+ * @example
+ *
+ * $redis->del('z');
+ * $redis->zAdd('key', 1, 'one');
+ * $redis->zAdd('key', 2, 'two');
+ * $redis->zRank('key', 'one'); // 0
+ * $redis->zRank('key', 'two'); // 1
+ * $redis->zRevRank('key', 'one'); // 1
+ * $redis->zRevRank('key', 'two'); // 0
+ *
+ */
+ public function zRank($key, $member)
+ {
+ }
+
+ /**
+ * @see zRank()
+ * @param string $key
+ * @param string|mixed $member
+ *
+ * @return int|bool the item's score, false - if key or member is not exists
+ *
+ * @link https://redis.io/commands/zrevrank
+ */
+ public function zRevRank($key, $member)
+ {
+ }
+
+ /**
+ * Increments the score of a member from a sorted set by a given amount.
+ *
+ * @param string $key
+ * @param float $value (double) value that will be added to the member's score
+ * @param string $member
+ *
+ * @return float the new value
+ *
+ * @link https://redis.io/commands/zincrby
+ * @example
+ *
+ * $redis->del('key');
+ * $redis->zIncrBy('key', 2.5, 'member1'); // key or member1 didn't exist, so member1's score is to 0
+ * // before the increment and now has the value 2.5
+ * $redis->zIncrBy('key', 1, 'member1'); // 3.5
+ *
+ */
+ public function zIncrBy($key, $value, $member)
+ {
+ }
+
+ /**
+ * Creates an union of sorted sets given in second argument.
+ * The result of the union will be stored in the sorted set defined by the first argument.
+ * The third optionnel argument defines weights to apply to the sorted sets in input.
+ * In this case, the weights will be multiplied by the score of each element in the sorted set
+ * before applying the aggregation. The forth argument defines the AGGREGATE option which
+ * specify how the results of the union are aggregated.
+ *
+ * @param string $output
+ * @param array $zSetKeys
+ * @param array $weights
+ * @param string $aggregateFunction Either "SUM", "MIN", or "MAX": defines the behaviour to use on
+ * duplicate entries during the zUnionStore
+ *
+ * @return int The number of values in the new sorted set
+ *
+ * @link https://redis.io/commands/zunionstore
+ * @example
+ *
+ * $redis->del('k1');
+ * $redis->del('k2');
+ * $redis->del('k3');
+ * $redis->del('ko1');
+ * $redis->del('ko2');
+ * $redis->del('ko3');
+ *
+ * $redis->zAdd('k1', 0, 'val0');
+ * $redis->zAdd('k1', 1, 'val1');
+ *
+ * $redis->zAdd('k2', 2, 'val2');
+ * $redis->zAdd('k2', 3, 'val3');
+ *
+ * $redis->zUnionStore('ko1', array('k1', 'k2')); // 4, 'ko1' => array('val0', 'val1', 'val2', 'val3')
+ *
+ * // Weighted zUnionStore
+ * $redis->zUnionStore('ko2', array('k1', 'k2'), array(1, 1)); // 4, 'ko2' => array('val0', 'val1', 'val2', 'val3')
+ * $redis->zUnionStore('ko3', array('k1', 'k2'), array(5, 1)); // 4, 'ko3' => array('val0', 'val2', 'val3', 'val1')
+ *
+ */
+ public function zUnionStore($output, $zSetKeys, array $weights = null, $aggregateFunction = 'SUM')
+ {
+ }
+
+ /**
+ * @see zUnionStore
+ * @deprecated use Redis::zUnionStore()
+ *
+ * @param string $Output
+ * @param array $ZSetKeys
+ * @param array|null $Weights
+ * @param string $aggregateFunction
+ */
+ public function zUnion($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
+ {
+ }
+
+ /**
+ * Creates an intersection of sorted sets given in second argument.
+ * The result of the union will be stored in the sorted set defined by the first argument.
+ * The third optional argument defines weights to apply to the sorted sets in input.
+ * In this case, the weights will be multiplied by the score of each element in the sorted set
+ * before applying the aggregation. The forth argument defines the AGGREGATE option which
+ * specify how the results of the union are aggregated.
+ *
+ * @param string $output
+ * @param array $zSetKeys
+ * @param array $weights
+ * @param string $aggregateFunction Either "SUM", "MIN", or "MAX":
+ * defines the behaviour to use on duplicate entries during the zInterStore.
+ *
+ * @return int The number of values in the new sorted set.
+ *
+ * @link https://redis.io/commands/zinterstore
+ * @example
+ *
+ * $redis->del('k1');
+ * $redis->del('k2');
+ * $redis->del('k3');
+ *
+ * $redis->del('ko1');
+ * $redis->del('ko2');
+ * $redis->del('ko3');
+ * $redis->del('ko4');
+ *
+ * $redis->zAdd('k1', 0, 'val0');
+ * $redis->zAdd('k1', 1, 'val1');
+ * $redis->zAdd('k1', 3, 'val3');
+ *
+ * $redis->zAdd('k2', 2, 'val1');
+ * $redis->zAdd('k2', 3, 'val3');
+ *
+ * $redis->zInterStore('ko1', array('k1', 'k2')); // 2, 'ko1' => array('val1', 'val3')
+ * $redis->zInterStore('ko2', array('k1', 'k2'), array(1, 1)); // 2, 'ko2' => array('val1', 'val3')
+ *
+ * // Weighted zInterStore
+ * $redis->zInterStore('ko3', array('k1', 'k2'), array(1, 5), 'min'); // 2, 'ko3' => array('val1', 'val3')
+ * $redis->zInterStore('ko4', array('k1', 'k2'), array(1, 5), 'max'); // 2, 'ko4' => array('val3', 'val1')
+ *
+ */
+ public function zInterStore($output, $zSetKeys, array $weights = null, $aggregateFunction = 'SUM')
+ {
+ }
+
+ /**
+ * @see zInterStore
+ * @deprecated use Redis::zInterStore()
+ *
+ * @param $Output
+ * @param $ZSetKeys
+ * @param array|null $Weights
+ * @param string $aggregateFunction
+ */
+ public function zInter($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
+ {
+ }
+
+ /**
+ * Scan a sorted set for members, with optional pattern and count
+ *
+ * @param string $key String, the set to scan.
+ * @param int $iterator Long (reference), initialized to NULL.
+ * @param string $pattern String (optional), the pattern to match.
+ * @param int $count How many keys to return per iteration (Redis might return a different number).
+ *
+ * @return array|bool PHPRedis will return matching keys from Redis, or FALSE when iteration is complete
+ *
+ * @link https://redis.io/commands/zscan
+ * @example
+ *
+ * $iterator = null;
+ * while ($members = $redis-zscan('zset', $iterator)) {
+ * foreach ($members as $member => $score) {
+ * echo $member . ' => ' . $score . PHP_EOL;
+ * }
+ * }
+ *
+ */
+ public function zScan($key, &$iterator, $pattern = null, $count = 0)
+ {
+ }
+
+ /**
+ * Block until Redis can pop the highest or lowest scoring members from one or more ZSETs.
+ * There are two commands (BZPOPMIN and BZPOPMAX for popping the lowest and highest scoring elements respectively.)
+ *
+ * @param string|array $key1
+ * @param string|array $key2 ...
+ * @param int $timeout
+ *
+ * @return array Either an array with the key member and score of the higest or lowest element or an empty array
+ * if the timeout was reached without an element to pop.
+ *
+ * @since >= 5.0
+ * @link https://redis.io/commands/bzpopmax
+ * @example
+ *
+ * // Wait up to 5 seconds to pop the *lowest* scoring member from sets `zs1` and `zs2`.
+ * $redis->bzPopMin(['zs1', 'zs2'], 5);
+ * $redis->bzPopMin('zs1', 'zs2', 5);
+ *
+ * // Wait up to 5 seconds to pop the *highest* scoring member from sets `zs1` and `zs2`
+ * $redis->bzPopMax(['zs1', 'zs2'], 5);
+ * $redis->bzPopMax('zs1', 'zs2', 5);
+ *
+ */
+ public function bzPopMax($key1, $key2, $timeout)
+ {
+ }
+
+ /**
+ * @param string|array $key1
+ * @param string|array $key2 ...
+ * @param int $timeout
+ *
+ * @return array Either an array with the key member and score of the higest or lowest element or an empty array
+ * if the timeout was reached without an element to pop.
+ *
+ * @see bzPopMax
+ * @since >= 5.0
+ * @link https://redis.io/commands/bzpopmin
+ */
+ public function bzPopMin($key1, $key2, $timeout)
+ {
+ }
+
+ /**
+ * Adds a value to the hash stored at key. If this value is already in the hash, FALSE is returned.
+ *
+ * @param string $key
+ * @param string $hashKey
+ * @param string $value
+ *
+ * @return int|bool
+ * - 1 if value didn't exist and was added successfully,
+ * - 0 if the value was already present and was replaced, FALSE if there was an error.
+ *
+ * @link https://redis.io/commands/hset
+ * @example
+ *
+ * $redis->del('h')
+ * $redis->hSet('h', 'key1', 'hello'); // 1, 'key1' => 'hello' in the hash at "h"
+ * $redis->hGet('h', 'key1'); // returns "hello"
+ *
+ * $redis->hSet('h', 'key1', 'plop'); // 0, value was replaced.
+ * $redis->hGet('h', 'key1'); // returns "plop"
+ *
+ */
+ public function hSet($key, $hashKey, $value)
+ {
+ }
+
+ /**
+ * Adds a value to the hash stored at key only if this field isn't already in the hash.
+ *
+ * @param string $key
+ * @param string $hashKey
+ * @param string $value
+ *
+ * @return bool TRUE if the field was set, FALSE if it was already present.
+ *
+ * @link https://redis.io/commands/hsetnx
+ * @example
+ *
+ * $redis->del('h')
+ * $redis->hSetNx('h', 'key1', 'hello'); // TRUE, 'key1' => 'hello' in the hash at "h"
+ * $redis->hSetNx('h', 'key1', 'world'); // FALSE, 'key1' => 'hello' in the hash at "h". No change since the field
+ * wasn't replaced.
+ *
+ */
+ public function hSetNx($key, $hashKey, $value)
+ {
+ }
+
+ /**
+ * Gets a value from the hash stored at key.
+ * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned.
+ *
+ * @param string $key
+ * @param string $hashKey
+ *
+ * @return string The value, if the command executed successfully BOOL FALSE in case of failure
+ *
+ * @link https://redis.io/commands/hget
+ */
+ public function hGet($key, $hashKey)
+ {
+ }
+
+ /**
+ * Returns the length of a hash, in number of items
+ *
+ * @param string $key
+ *
+ * @return int|false the number of items in a hash, FALSE if the key doesn't exist or isn't a hash
+ *
+ * @link https://redis.io/commands/hlen
+ * @example
+ *
+ * $redis->del('h')
+ * $redis->hSet('h', 'key1', 'hello');
+ * $redis->hSet('h', 'key2', 'plop');
+ * $redis->hLen('h'); // returns 2
+ *
+ */
+ public function hLen($key)
+ {
+ }
+
+ /**
+ * Removes a values from the hash stored at key.
+ * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned.
+ *
+ * @param string $key
+ * @param string $hashKey1
+ * @param string ...$otherHashKeys
+ *
+ * @return int|false Number of deleted fields
+ *
+ * @link https://redis.io/commands/hdel
+ * @example
+ *
+ * $redis->hMSet('h',
+ * array(
+ * 'f1' => 'v1',
+ * 'f2' => 'v2',
+ * 'f3' => 'v3',
+ * 'f4' => 'v4',
+ * ));
+ *
+ * var_dump( $redis->hDel('h', 'f1') ); // int(1)
+ * var_dump( $redis->hDel('h', 'f2', 'f3') ); // int(2)
+ * s
+ * var_dump( $redis->hGetAll('h') );
+ * //// Output:
+ * // array(1) {
+ * // ["f4"]=> string(2) "v4"
+ * // }
+ *
+ */
+ public function hDel($key, $hashKey1, ...$otherHashKeys)
+ {
+ }
+
+ /**
+ * Returns the keys in a hash, as an array of strings.
+ *
+ * @param string $key
+ *
+ * @return array An array of elements, the keys of the hash. This works like PHP's array_keys().
+ *
+ * @link https://redis.io/commands/hkeys
+ * @example
+ *
+ * $redis->del('h');
+ * $redis->hSet('h', 'a', 'x');
+ * $redis->hSet('h', 'b', 'y');
+ * $redis->hSet('h', 'c', 'z');
+ * $redis->hSet('h', 'd', 't');
+ * var_dump($redis->hKeys('h'));
+ *
+ * // Output:
+ * // array(4) {
+ * // [0]=>
+ * // string(1) "a"
+ * // [1]=>
+ * // string(1) "b"
+ * // [2]=>
+ * // string(1) "c"
+ * // [3]=>
+ * // string(1) "d"
+ * // }
+ * // The order is random and corresponds to redis' own internal representation of the set structure.
+ *
+ */
+ public function hKeys($key)
+ {
+ }
+
+ /**
+ * Returns the values in a hash, as an array of strings.
+ *
+ * @param string $key
+ *
+ * @return array An array of elements, the values of the hash. This works like PHP's array_values().
+ *
+ * @link https://redis.io/commands/hvals
+ * @example
+ *
+ * $redis->del('h');
+ * $redis->hSet('h', 'a', 'x');
+ * $redis->hSet('h', 'b', 'y');
+ * $redis->hSet('h', 'c', 'z');
+ * $redis->hSet('h', 'd', 't');
+ * var_dump($redis->hVals('h'));
+ *
+ * // Output
+ * // array(4) {
+ * // [0]=>
+ * // string(1) "x"
+ * // [1]=>
+ * // string(1) "y"
+ * // [2]=>
+ * // string(1) "z"
+ * // [3]=>
+ * // string(1) "t"
+ * // }
+ * // The order is random and corresponds to redis' own internal representation of the set structure.
+ *
+ */
+ public function hVals($key)
+ {
+ }
+
+ /**
+ * Returns the whole hash, as an array of strings indexed by strings.
+ *
+ * @param string $key
+ *
+ * @return array An array of elements, the contents of the hash.
+ *
+ * @link https://redis.io/commands/hgetall
+ * @example
+ *
+ * $redis->del('h');
+ * $redis->hSet('h', 'a', 'x');
+ * $redis->hSet('h', 'b', 'y');
+ * $redis->hSet('h', 'c', 'z');
+ * $redis->hSet('h', 'd', 't');
+ * var_dump($redis->hGetAll('h'));
+ *
+ * // Output:
+ * // array(4) {
+ * // ["a"]=>
+ * // string(1) "x"
+ * // ["b"]=>
+ * // string(1) "y"
+ * // ["c"]=>
+ * // string(1) "z"
+ * // ["d"]=>
+ * // string(1) "t"
+ * // }
+ * // The order is random and corresponds to redis' own internal representation of the set structure.
+ *
+ */
+ public function hGetAll($key)
+ {
+ }
+
+ /**
+ * Verify if the specified member exists in a key.
+ *
+ * @param string $key
+ * @param string $hashKey
+ *
+ * @return bool If the member exists in the hash table, return TRUE, otherwise return FALSE.
+ *
+ * @link https://redis.io/commands/hexists
+ * @example
+ *
+ * $redis->hSet('h', 'a', 'x');
+ * $redis->hExists('h', 'a'); // TRUE
+ * $redis->hExists('h', 'NonExistingKey'); // FALSE
+ *
+ */
+ public function hExists($key, $hashKey)
+ {
+ }
+
+ /**
+ * Increments the value of a member from a hash by a given amount.
+ *
+ * @param string $key
+ * @param string $hashKey
+ * @param int $value (integer) value that will be added to the member's value
+ *
+ * @return int the new value
+ *
+ * @link https://redis.io/commands/hincrby
+ * @example
+ *
+ * $redis->del('h');
+ * $redis->hIncrBy('h', 'x', 2); // returns 2: h[x] = 2 now.
+ * $redis->hIncrBy('h', 'x', 1); // h[x] ← 2 + 1. Returns 3
+ *
+ */
+ public function hIncrBy($key, $hashKey, $value)
+ {
+ }
+
+ /**
+ * Increment the float value of a hash field by the given amount
+ *
+ * @param string $key
+ * @param string $field
+ * @param float $increment
+ *
+ * @return float
+ *
+ * @link https://redis.io/commands/hincrbyfloat
+ * @example
+ *
+ * $redis = new Redis();
+ * $redis->connect('127.0.0.1');
+ * $redis->hset('h', 'float', 3);
+ * $redis->hset('h', 'int', 3);
+ * var_dump( $redis->hIncrByFloat('h', 'float', 1.5) ); // float(4.5)
+ *
+ * var_dump( $redis->hGetAll('h') );
+ *
+ * // Output
+ * array(2) {
+ * ["float"]=>
+ * string(3) "4.5"
+ * ["int"]=>
+ * string(1) "3"
+ * }
+ *
+ */
+ public function hIncrByFloat($key, $field, $increment)
+ {
+ }
+
+ /**
+ * Fills in a whole hash. Non-string values are converted to string, using the standard (string) cast.
+ * NULL values are stored as empty strings
+ *
+ * @param string $key
+ * @param array $hashKeys key → value array
+ *
+ * @return bool
+ *
+ * @link https://redis.io/commands/hmset
+ * @example
+ *
+ * $redis->del('user:1');
+ * $redis->hMSet('user:1', array('name' => 'Joe', 'salary' => 2000));
+ * $redis->hIncrBy('user:1', 'salary', 100); // Joe earns 100 more now.
+ *
+ */
+ public function hMSet($key, $hashKeys)
+ {
+ }
+
+ /**
+ * Retirieve the values associated to the specified fields in the hash.
+ *
+ * @param string $key
+ * @param array $hashKeys
+ *
+ * @return array Array An array of elements, the values of the specified fields in the hash,
+ * with the hash keys as array keys.
+ *
+ * @link https://redis.io/commands/hmget
+ * @example
+ *
+ * $redis->del('h');
+ * $redis->hSet('h', 'field1', 'value1');
+ * $redis->hSet('h', 'field2', 'value2');
+ * $redis->hmGet('h', array('field1', 'field2')); // returns array('field1' => 'value1', 'field2' => 'value2')
+ *
+ */
+ public function hMGet($key, $hashKeys)
+ {
+ }
+
+ /**
+ * Scan a HASH value for members, with an optional pattern and count.
+ *
+ * @param string $key
+ * @param int $iterator
+ * @param string $pattern Optional pattern to match against.
+ * @param int $count How many keys to return in a go (only a sugestion to Redis).
+ *
+ * @return array An array of members that match our pattern.
+ *
+ * @link https://redis.io/commands/hscan
+ * @example
+ *
+ * // $iterator = null;
+ * // while($elements = $redis->hscan('hash', $iterator)) {
+ * // foreach($elements as $key => $value) {
+ * // echo $key . ' => ' . $value . PHP_EOL;
+ * // }
+ * // }
+ *
+ */
+ public function hScan($key, &$iterator, $pattern = null, $count = 0)
+ {
+ }
+
+ /**
+ * Get the string length of the value associated with field in the hash stored at key
+ *
+ * @param string $key
+ * @param string $field
+ *
+ * @return int the string length of the value associated with field, or zero when field is not present in the hash
+ * or key does not exist at all.
+ *
+ * @link https://redis.io/commands/hstrlen
+ * @since >= 3.2
+ */
+ public function hStrLen(string $key, string $field)
+ {
+ }
+
+ /**
+ * Add one or more geospatial items to the specified key.
+ * This function must be called with at least one longitude, latitude, member triplet.
+ *
+ * @param string $key
+ * @param float $longitude
+ * @param float $latitude
+ * @param string $member
+ *
+ * @return int The number of elements added to the geospatial key
+ *
+ * @link https://redis.io/commands/geoadd
+ * @since >=3.2
+ *
+ * @example
+ *
+ * $redis->del("myplaces");
+ *
+ * // Since the key will be new, $result will be 2
+ * $result = $redis->geoAdd(
+ * "myplaces",
+ * -122.431, 37.773, "San Francisco",
+ * -157.858, 21.315, "Honolulu"
+ * ); // 2
+ *
+ */
+ public function geoadd($key, $longitude, $latitude, $member)
+ {
+ }
+
+ /**
+ * Retrieve Geohash strings for one or more elements of a geospatial index.
+
+ * @param string $key
+ * @param string ...$member variadic list of members
+ *
+ * @return array One or more Redis Geohash encoded strings
+ *
+ * @link https://redis.io/commands/geohash
+ * @since >=3.2
+ *
+ * @example
+ *
+ * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
+ * $hashes = $redis->geoHash("hawaii", "Honolulu", "Maui");
+ * var_dump($hashes);
+ * // Output: array(2) {
+ * // [0]=>
+ * // string(11) "87z9pyek3y0"
+ * // [1]=>
+ * // string(11) "8e8y6d5jps0"
+ * // }
+ *
+ */
+ public function geohash($key, ...$member)
+ {
+ }
+
+ /**
+ * Return longitude, latitude positions for each requested member.
+ *
+ * @param string $key
+ * @param string $member
+ * @return array One or more longitude/latitude positions
+ *
+ * @link https://redis.io/commands/geopos
+ * @since >=3.2
+ *
+ * @example
+ *
+ * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
+ * $positions = $redis->geoPos("hawaii", "Honolulu", "Maui");
+ * var_dump($positions);
+ *
+ * // Output:
+ * array(2) {
+ * [0]=> array(2) {
+ * [0]=> string(22) "-157.85800248384475708"
+ * [1]=> string(19) "21.3060004581273077"
+ * }
+ * [1]=> array(2) {
+ * [0]=> string(22) "-156.33099943399429321"
+ * [1]=> string(20) "20.79799924753607598"
+ * }
+ * }
+ *
+ */
+ public function geopos(string $key, string $member)
+ {
+ }
+
+ /**
+ * Return the distance between two members in a geospatial set.
+ *
+ * If units are passed it must be one of the following values:
+ * - 'm' => Meters
+ * - 'km' => Kilometers
+ * - 'mi' => Miles
+ * - 'ft' => Feet
+ *
+ * @param string $key
+ * @param string $member1
+ * @param string $member2
+ * @param string|null $unit
+ *
+ * @return float The distance between the two passed members in the units requested (meters by default)
+ *
+ * @link https://redis.io/commands/geodist
+ * @since >=3.2
+ *
+ * @example
+ *
+ * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
+ *
+ * $meters = $redis->geoDist("hawaii", "Honolulu", "Maui");
+ * $kilometers = $redis->geoDist("hawaii", "Honolulu", "Maui", 'km');
+ * $miles = $redis->geoDist("hawaii", "Honolulu", "Maui", 'mi');
+ * $feet = $redis->geoDist("hawaii", "Honolulu", "Maui", 'ft');
+ *
+ * echo "Distance between Honolulu and Maui:\n";
+ * echo " meters : $meters\n";
+ * echo " kilometers: $kilometers\n";
+ * echo " miles : $miles\n";
+ * echo " feet : $feet\n";
+ *
+ * // Bad unit
+ * $inches = $redis->geoDist("hawaii", "Honolulu", "Maui", 'in');
+ * echo "Invalid unit returned:\n";
+ * var_dump($inches);
+ *
+ * // Output
+ * Distance between Honolulu and Maui:
+ * meters : 168275.204
+ * kilometers: 168.2752
+ * miles : 104.5616
+ * feet : 552084.0028
+ * Invalid unit returned:
+ * bool(false)
+ *
+ */
+ public function geodist($key, $member1, $member2, $unit = null)
+ {
+ }
+
+ /**
+ * Return members of a set with geospatial information that are within the radius specified by the caller.
+ *
+ * @param $key
+ * @param $longitude
+ * @param $latitude
+ * @param $radius
+ * @param $unit
+ * @param array|null $options
+ * + * |Key |Value |Description | + * |------------|---------------|---------------------------------------------------| + * |COUNT |integer > 0 |Limit how many results are returned | + * | |WITHCOORD |Return longitude and latitude of matching members | + * | |WITHDIST |Return the distance from the center | + * | |WITHHASH |Return the raw geohash-encoded score | + * | |ASC |Sort results in ascending order | + * | |DESC |Sort results in descending order | + * |STORE |key |Store results in key | + * |STOREDIST |key |Store the results as distances in key | + *+ * Note: It doesn't make sense to pass both ASC and DESC options but if both are passed + * the last one passed will be used. + * Note: When using STORE[DIST] in Redis Cluster, the store key must has to the same slot as + * the query key or you will get a CROSSLOT error. + * @return mixed When no STORE option is passed, this function returns an array of results. + * If it is passed this function returns the number of stored entries. + * + * @link https://redis.io/commands/georadius + * @since >= 3.2 + * @example + *
+ * // Add some cities
+ * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
+ *
+ * echo "Within 300 miles of Honolulu:\n";
+ * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi'));
+ *
+ * echo "\nWithin 300 miles of Honolulu with distances:\n";
+ * $options = ['WITHDIST'];
+ * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
+ *
+ * echo "\nFirst result within 300 miles of Honolulu with distances:\n";
+ * $options['count'] = 1;
+ * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
+ *
+ * echo "\nFirst result within 300 miles of Honolulu with distances in descending sort order:\n";
+ * $options[] = 'DESC';
+ * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
+ *
+ * // Output
+ * Within 300 miles of Honolulu:
+ * array(2) {
+ * [0]=> string(8) "Honolulu"
+ * [1]=> string(4) "Maui"
+ * }
+ *
+ * Within 300 miles of Honolulu with distances:
+ * array(2) {
+ * [0]=>
+ * array(2) {
+ * [0]=>
+ * string(8) "Honolulu"
+ * [1]=>
+ * string(6) "0.0002"
+ * }
+ * [1]=>
+ * array(2) {
+ * [0]=>
+ * string(4) "Maui"
+ * [1]=>
+ * string(8) "104.5615"
+ * }
+ * }
+ *
+ * First result within 300 miles of Honolulu with distances:
+ * array(1) {
+ * [0]=>
+ * array(2) {
+ * [0]=>
+ * string(8) "Honolulu"
+ * [1]=>
+ * string(6) "0.0002"
+ * }
+ * }
+ *
+ * First result within 300 miles of Honolulu with distances in descending sort order:
+ * array(1) {
+ * [0]=>
+ * array(2) {
+ * [0]=>
+ * string(4) "Maui"
+ * [1]=>
+ * string(8) "104.5615"
+ * }
+ * }
+ *
+ */
+ public function georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
+ {
+ }
+
+ /**
+ * This method is identical to geoRadius except that instead of passing a longitude and latitude as the "source"
+ * you pass an existing member in the geospatial set
+ *
+ * @param string $key
+ * @param string $member
+ * @param $radius
+ * @param $units
+ * @param array|null $options see georadius
+ *
+ * @return array The zero or more entries that are close enough to the member given the distance and radius specified
+ *
+ * @link https://redis.io/commands/georadiusbymember
+ * @since >= 3.2
+ * @see georadius
+ * @example
+ *
+ * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
+ *
+ * echo "Within 300 miles of Honolulu:\n";
+ * var_dump($redis->geoRadiusByMember("hawaii", "Honolulu", 300, 'mi'));
+ *
+ * echo "\nFirst match within 300 miles of Honolulu:\n";
+ * var_dump($redis->geoRadiusByMember("hawaii", "Honolulu", 300, 'mi', ['count' => 1]));
+ *
+ * // Output
+ * Within 300 miles of Honolulu:
+ * array(2) {
+ * [0]=> string(8) "Honolulu"
+ * [1]=> string(4) "Maui"
+ * }
+ *
+ * First match within 300 miles of Honolulu:
+ * array(1) {
+ * [0]=> string(8) "Honolulu"
+ * }
+ *
+ */
+ public function georadiusbymember($key, $member, $radius, $units, array $options = null)
+ {
+ }
+
+ /**
+ * Get or Set the redis config keys.
+ *
+ * @param string $operation either `GET` or `SET`
+ * @param string $key for `SET`, glob-pattern for `GET`
+ * @param string|mixed $value optional string (only for `SET`)
+ *
+ * @return array Associative array for `GET`, key -> value
+ *
+ * @link https://redis.io/commands/config-get
+ * @example
+ *
+ * $redis->config("GET", "*max-*-entries*");
+ * $redis->config("SET", "dir", "/var/run/redis/dumps/");
+ *
+ */
+ public function config($operation, $key, $value)
+ {
+ }
+
+ /**
+ * Evaluate a LUA script serverside
+ *
+ * @param string $script
+ * @param array $args
+ * @param int $numKeys
+ *
+ * @return mixed What is returned depends on what the LUA script itself returns, which could be a scalar value
+ * (int/string), or an array. Arrays that are returned can also contain other arrays, if that's how it was set up in
+ * your LUA script. If there is an error executing the LUA script, the getLastError() function can tell you the
+ * message that came back from Redis (e.g. compile error).
+ *
+ * @link https://redis.io/commands/eval
+ * @example
+ *
+ * $redis->eval("return 1"); // Returns an integer: 1
+ * $redis->eval("return {1,2,3}"); // Returns Array(1,2,3)
+ * $redis->del('mylist');
+ * $redis->rpush('mylist','a');
+ * $redis->rpush('mylist','b');
+ * $redis->rpush('mylist','c');
+ * // Nested response: Array(1,2,3,Array('a','b','c'));
+ * $redis->eval("return {1,2,3,redis.call('lrange','mylist',0,-1)}}");
+ *
+ */
+ public function eval($script, $args = array(), $numKeys = 0)
+ {
+ }
+
+ /**
+ * @see eval()
+ * @deprecated use Redis::eval()
+ *
+ * @param string $script
+ * @param array $args
+ * @param int $numKeys
+ * @return mixed @see eval()
+ */
+ public function evaluate($script, $args = array(), $numKeys = 0)
+ {
+ }
+
+ /**
+ * Evaluate a LUA script serverside, from the SHA1 hash of the script instead of the script itself.
+ * In order to run this command Redis will have to have already loaded the script, either by running it or via
+ * the SCRIPT LOAD command.
+ *
+ * @param string $scriptSha
+ * @param array $args
+ * @param int $numKeys
+ *
+ * @return mixed @see eval()
+ *
+ * @see eval()
+ * @link https://redis.io/commands/evalsha
+ * @example
+ *
+ * $script = 'return 1';
+ * $sha = $redis->script('load', $script);
+ * $redis->evalSha($sha); // Returns 1
+ *
+ */
+ public function evalSha($scriptSha, $args = array(), $numKeys = 0)
+ {
+ }
+
+ /**
+ * @see evalSha()
+ * @deprecated use Redis::evalSha()
+ *
+ * @param string $scriptSha
+ * @param array $args
+ * @param int $numKeys
+ */
+ public function evaluateSha($scriptSha, $args = array(), $numKeys = 0)
+ {
+ }
+
+ /**
+ * Execute the Redis SCRIPT command to perform various operations on the scripting subsystem.
+ * @param string $command load | flush | kill | exists
+ * @param string $script
+ *
+ * @return mixed
+ *
+ * @link https://redis.io/commands/script-load
+ * @link https://redis.io/commands/script-kill
+ * @link https://redis.io/commands/script-flush
+ * @link https://redis.io/commands/script-exists
+ * @example
+ *
+ * $redis->script('load', $script);
+ * $redis->script('flush');
+ * $redis->script('kill');
+ * $redis->script('exists', $script1, [$script2, $script3, ...]);
+ *
+ *
+ * SCRIPT LOAD will return the SHA1 hash of the passed script on success, and FALSE on failure.
+ * SCRIPT FLUSH should always return TRUE
+ * SCRIPT KILL will return true if a script was able to be killed and false if not
+ * SCRIPT EXISTS will return an array with TRUE or FALSE for each passed script
+ */
+ public function script($command, $script)
+ {
+ }
+
+ /**
+ * The last error message (if any)
+ *
+ * @return string|null A string with the last returned script based error message, or NULL if there is no error
+ *
+ * @example
+ *
+ * $redis->eval('this-is-not-lua');
+ * $err = $redis->getLastError();
+ * // "ERR Error compiling script (new function): user_script:1: '=' expected near '-'"
+ *
+ */
+ public function getLastError()
+ {
+ }
+
+ /**
+ * Clear the last error message
+ *
+ * @return bool true
+ *
+ * @example
+ *
+ * $redis->set('x', 'a');
+ * $redis->incr('x');
+ * $err = $redis->getLastError();
+ * // "ERR value is not an integer or out of range"
+ * $redis->clearLastError();
+ * $err = $redis->getLastError();
+ * // NULL
+ *
+ */
+ public function clearLastError()
+ {
+ }
+
+ /**
+ * Issue the CLIENT command with various arguments.
+ * The Redis CLIENT command can be used in four ways:
+ * - CLIENT LIST
+ * - CLIENT GETNAME
+ * - CLIENT SETNAME [name]
+ * - CLIENT KILL [ip:port]
+ *
+ * @param string $command
+ * @param string $value
+ * @return mixed This will vary depending on which client command was executed:
+ * - CLIENT LIST will return an array of arrays with client information.
+ * - CLIENT GETNAME will return the client name or false if none has been set
+ * - CLIENT SETNAME will return true if it can be set and false if not
+ * - CLIENT KILL will return true if the client can be killed, and false if not
+ *
+ * Note: phpredis will attempt to reconnect so you can actually kill your own connection but may not notice losing it!
+ *
+ * @link https://redis.io/commands/client-list
+ * @link https://redis.io/commands/client-getname
+ * @link https://redis.io/commands/client-setname
+ * @link https://redis.io/commands/client-kill
+ *
+ * @example
+ *
+ * $redis->client('list'); // Get a list of clients
+ * $redis->client('getname'); // Get the name of the current connection
+ * $redis->client('setname', 'somename'); // Set the name of the current connection
+ * $redis->client('kill', ); // Kill the process at ip:port
+ *
+ */
+ public function client($command, $value = '')
+ {
+ }
+
+ /**
+ * A utility method to prefix the value with the prefix setting for phpredis.
+ *
+ * @param mixed $value The value you wish to prefix
+ *
+ * @return string If a prefix is set up, the value now prefixed.
+ * If there is no prefix, the value will be returned unchanged.
+ *
+ * @example
+ *
+ * $redis->setOption(Redis::OPT_PREFIX, 'my-prefix:');
+ * $redis->_prefix('my-value'); // Will return 'my-prefix:my-value'
+ *
+ */
+ public function _prefix($value)
+ {
+ }
+
+ /**
+ * A utility method to unserialize data with whatever serializer is set up. If there is no serializer set, the
+ * value will be returned unchanged. If there is a serializer set up, and the data passed in is malformed, an
+ * exception will be thrown. This can be useful if phpredis is serializing values, and you return something from
+ * redis in a LUA script that is serialized.
+ *
+ * @param string $value The value to be unserialized
+ *
+ * @return mixed
+ * @example
+ *
+ * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
+ * $redis->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)
+ *
+ */
+ public function _unserialize($value)
+ {
+ }
+
+ /**
+ * A utility method to serialize values manually. This method allows you to serialize a value with whatever
+ * serializer is configured, manually. This can be useful for serialization/unserialization of data going in
+ * and out of EVAL commands as phpredis can't automatically do this itself. Note that if no serializer is
+ * set, phpredis will change Array values to 'Array', and Objects to 'Object'.
+ *
+ * @param mixed $value The value to be serialized.
+ *
+ * @return mixed
+ * @example
+ *
+ * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
+ * $redis->_serialize("foo"); // returns "foo"
+ * $redis->_serialize(Array()); // Returns "Array"
+ * $redis->_serialize(new stdClass()); // Returns "Object"
+ *
+ * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
+ * $redis->_serialize("foo"); // Returns 's:3:"foo";'
+ *
+ */
+ public function _serialize($value)
+ {
+ }
+
+ /**
+ * Dump a key out of a redis database, the value of which can later be passed into redis using the RESTORE command.
+ * The data that comes out of DUMP is a binary representation of the key as Redis stores it.
+ * @param string $key
+ *
+ * @return string|bool The Redis encoded value of the key, or FALSE if the key doesn't exist
+ *
+ * @link https://redis.io/commands/dump
+ * @example
+ *
+ * $redis->set('foo', 'bar');
+ * $val = $redis->dump('foo'); // $val will be the Redis encoded key value
+ *
+ */
+ public function dump($key)
+ {
+ }
+
+ /**
+ * Restore a key from the result of a DUMP operation.
+ *
+ * @param string $key The key name
+ * @param int $ttl How long the key should live (if zero, no expire will be set on the key)
+ * @param string $value (binary). The Redis encoded key value (from DUMP)
+ *
+ * @return bool
+ *
+ * @link https://redis.io/commands/restore
+ * @example
+ *
+ * $redis->set('foo', 'bar');
+ * $val = $redis->dump('foo');
+ * $redis->restore('bar', 0, $val); // The key 'bar', will now be equal to the key 'foo'
+ *
+ */
+ public function restore($key, $ttl, $value)
+ {
+ }
+
+ /**
+ * Migrates a key to a different Redis instance.
+ *
+ * @param string $host The destination host
+ * @param int $port The TCP port to connect to.
+ * @param string $key The key to migrate.
+ * @param int $db The target DB.
+ * @param int $timeout The maximum amount of time given to this transfer.
+ * @param bool $copy Should we send the COPY flag to redis.
+ * @param bool $replace Should we send the REPLACE flag to redis.
+ *
+ * @return bool
+ *
+ * @link https://redis.io/commands/migrate
+ * @example
+ *
+ * $redis->migrate('backup', 6379, 'foo', 0, 3600);
+ *
+ */
+ public function migrate($host, $port, $key, $db, $timeout, $copy = false, $replace = false)
+ {
+ }
+
+ /**
+ * Return the current Redis server time.
+ *
+ * @return array If successfull, the time will come back as an associative array with element zero being the
+ * unix timestamp, and element one being microseconds.
+ *
+ * @link https://redis.io/commands/time
+ * @example
+ *
+ * var_dump( $redis->time() );
+ * // array(2) {
+ * // [0] => string(10) "1342364352"
+ * // [1] => string(6) "253002"
+ * // }
+ *
+ */
+ public function time()
+ {
+ }
+
+ /**
+ * Scan the keyspace for keys
+ *
+ * @param int $iterator Iterator, initialized to NULL.
+ * @param string $pattern Pattern to match.
+ * @param int $count Count of keys per iteration (only a suggestion to Redis).
+ *
+ * @return array|bool This function will return an array of keys or FALSE if there are no more keys.
+ *
+ * @link https://redis.io/commands/scan
+ * @example
+ *
+ * $iterator = null;
+ * while(false !== ($keys = $redis->scan($iterator))) {
+ * foreach($keys as $key) {
+ * echo $key . PHP_EOL;
+ * }
+ * }
+ *
+ */
+ public function scan(&$iterator, $pattern = null, $count = 0)
+ {
+ }
+
+ /**
+ * Adds all the element arguments to the HyperLogLog data structure stored at the key.
+ *
+ * @param string $key
+ * @param array $elements
+ *
+ * @return bool
+ *
+ * @link https://redis.io/commands/pfadd
+ * @example $redis->pfAdd('key', array('elem1', 'elem2'))
+ */
+ public function pfAdd($key, array $elements)
+ {
+ }
+
+ /**
+ * When called with a single key, returns the approximated cardinality computed by the HyperLogLog data
+ * structure stored at the specified variable, which is 0 if the variable does not exist.
+ *
+ * @param string|array $key
+ *
+ * @return int
+ *
+ * @link https://redis.io/commands/pfcount
+ * @example
+ *
+ * $redis->pfAdd('key1', array('elem1', 'elem2'));
+ * $redis->pfAdd('key2', array('elem3', 'elem2'));
+ * $redis->pfCount('key1'); // int(2)
+ * $redis->pfCount(array('key1', 'key2')); // int(3)
+ */
+ public function pfCount($key)
+ {
+ }
+
+ /**
+ * Merge multiple HyperLogLog values into an unique value that will approximate the cardinality
+ * of the union of the observed Sets of the source HyperLogLog structures.
+ *
+ * @param string $destKey
+ * @param array $sourceKeys
+ *
+ * @return bool
+ *
+ * @link https://redis.io/commands/pfmerge
+ * @example
+ *
+ * $redis->pfAdd('key1', array('elem1', 'elem2'));
+ * $redis->pfAdd('key2', array('elem3', 'elem2'));
+ * $redis->pfMerge('key3', array('key1', 'key2'));
+ * $redis->pfCount('key3'); // int(3)
+ */
+ public function pfMerge($destKey, array $sourceKeys)
+ {
+ }
+
+ /**
+ * Send arbitrary things to the redis server.
+ *
+ * @param string $command Required command to send to the server.
+ * @param mixed $arguments Optional variable amount of arguments to send to the server.
+ *
+ * @return mixed
+ *
+ * @example
+ *
+ * $redis->rawCommand('SET', 'key', 'value'); // bool(true)
+ * $redis->rawCommand('GET", 'key'); // string(5) "value"
+ *
+ */
+ public function rawCommand($command, $arguments)
+ {
+ }
+
+ /**
+ * Detect whether we're in ATOMIC/MULTI/PIPELINE mode.
+ *
+ * @return int Either Redis::ATOMIC, Redis::MULTI or Redis::PIPELINE
+ *
+ * @example $redis->getMode();
+ */
+ public function getMode()
+ {
+ }
+
+ /**
+ * Acknowledge one or more messages on behalf of a consumer group.
+ *
+ * @param string $stream
+ * @param string $group
+ * @param array $messages
+ *
+ * @return int The number of messages Redis reports as acknowledged.
+ *
+ * @link https://redis.io/commands/xack
+ * @example
+ *
+ * $redis->xAck('stream', 'group1', ['1530063064286-0', '1530063064286-1']);
+ *
+ */
+ public function xAck($stream, $group, $messages)
+ {
+ }
+
+ /**
+ * Add a message to a stream
+ *
+ * @param string $key
+ * @param string $id
+ * @param array $messages
+ * @param int $maxLen
+ * @param bool $isApproximate
+ *
+ * @return string The added message ID.
+ *
+ * @link https://redis.io/commands/xadd
+ * @example
+ *
+ * $redis->xAdd('mystream', "*", ['field' => 'value']);
+ * $redis->xAdd('mystream', "*", ['field' => 'value'], 10);
+ * $redis->xAdd('mystream', "*", ['field' => 'value'], 10, true);
+ *
+ */
+ public function xAdd($key, $id, $messages, $maxLen = 0, $isApproximate = false)
+ {
+ }
+
+ /**
+ * Claim ownership of one or more pending messages
+ *
+ * @param string $key
+ * @param string $group
+ * @param string $consumer
+ * @param int $minIdleTime
+ * @param array $ids
+ * @param array $options ['IDLE' => $value, 'TIME' => $value, 'RETRYCOUNT' => $value, 'FORCE', 'JUSTID']
+ *
+ * @return array Either an array of message IDs along with corresponding data, or just an array of IDs
+ * (if the 'JUSTID' option was passed).
+ *
+ * @link https://redis.io/commands/xclaim
+ * @example
+ *
+ * $ids = ['1530113681011-0', '1530113681011-1', '1530113681011-2'];
+ *
+ * // Without any options
+ * $redis->xClaim('mystream', 'group1', 'myconsumer1', 0, $ids);
+ *
+ * // With options
+ * $redis->xClaim(
+ * 'mystream', 'group1', 'myconsumer2', 0, $ids,
+ * [
+ * 'IDLE' => time() * 1000,
+ * 'RETRYCOUNT' => 5,
+ * 'FORCE',
+ * 'JUSTID'
+ * ]
+ * );
+ *
+ */
+ public function xClaim($key, $group, $consumer, $minIdleTime, $ids, $options = [])
+ {
+ }
+
+ /**
+ * Delete one or more messages from a stream
+ *
+ * @param string $key
+ * @param array $ids
+ *
+ * @return int The number of messages removed
+ *
+ * @link https://redis.io/commands/xdel
+ * @example
+ *
+ * $redis->xDel('mystream', ['1530115304877-0', '1530115305731-0']);
+ *
+ */
+ public function xDel($key, $ids)
+ {
+ }
+
+ /**
+ * @param string $operation e.g.: 'HELP', 'SETID', 'DELGROUP', 'CREATE', 'DELCONSUMER'
+ * @param string $key
+ * @param string $group
+ * @param string $msgId
+ * @param bool $mkStream
+ *
+ * @return mixed This command returns different types depending on the specific XGROUP command executed.
+ *
+ * @link https://redis.io/commands/xgroup
+ * @example
+ *
+ * $redis->xGroup('CREATE', 'mystream', 'mygroup', 0);
+ * $redis->xGroup('CREATE', 'mystream', 'mygroup', 0, true); // create stream
+ * $redis->xGroup('DESTROY', 'mystream', 'mygroup');
+ *
+ */
+ public function xGroup($operation, $key, $group, $msgId = '', $mkStream = false)
+ {
+ }
+
+ /**
+ * Get information about a stream or consumer groups
+ *
+ * @param string $operation e.g.: 'CONSUMERS', 'GROUPS', 'STREAM', 'HELP'
+ * @param string $stream
+ * @param string $group
+ *
+ * @return mixed This command returns different types depending on which subcommand is used.
+ *
+ * @link https://redis.io/commands/xinfo
+ * @example
+ *
+ * $redis->xInfo('STREAM', 'mystream');
+ *
+ */
+ public function xInfo($operation, $stream, $group)
+ {
+ }
+
+ /**
+ * Get the length of a given stream.
+ *
+ * @param string $stream
+ *
+ * @return int The number of messages in the stream.
+ *
+ * @link https://redis.io/commands/xlen
+ * @example
+ *
+ * $redis->xLen('mystream');
+ *
+ */
+ public function xLen($stream)
+ {
+ }
+
+ /**
+ * Get information about pending messages in a given stream
+ *
+ * @param string $stream
+ * @param string $group
+ * @param string $start
+ * @param string $end
+ * @param int $count
+ * @param string $consumer
+ *
+ * @return array Information about the pending messages, in various forms depending on
+ * the specific invocation of XPENDING.
+ *
+ * @link https://redis.io/commands/xpending
+ * @example
+ *
+ * $redis->xPending('mystream', 'mygroup');
+ * $redis->xPending('mystream', 'mygroup', '-', '+', 1, 'consumer-1');
+ *
+ */
+ public function xPending($stream, $group, $start = null, $end = null, $count = null, $consumer = null)
+ {
+ }
+
+ /**
+ * Get a range of messages from a given stream
+ *
+ * @param string $stream
+ * @param string $start
+ * @param string $end
+ * @param int $count
+ *
+ * @return array The messages in the stream within the requested range.
+ *
+ * @link https://redis.io/commands/xrange
+ * @example
+ *
+ * // Get everything in this stream
+ * $redis->xRange('mystream', '-', '+');
+ * // Only the first two messages
+ * $redis->xRange('mystream', '-', '+', 2);
+ *
+ */
+ public function xRange($stream, $start, $end, $count = null)
+ {
+ }
+
+ /**
+ * Read data from one or more streams and only return IDs greater than sent in the command.
+ *
+ * @param array $streams
+ * @param int|string $count
+ * @param int|string $block
+ *
+ * @return array The messages in the stream newer than the IDs passed to Redis (if any)
+ *
+ * @link https://redis.io/commands/xread
+ * @example
+ *
+ * $redis->xRead(['stream1' => '1535222584555-0', 'stream2' => '1535222584555-0']);
+ *
+ */
+ public function xRead($streams, $count = null, $block = null)
+ {
+ }
+
+ /**
+ * This method is similar to xRead except that it supports reading messages for a specific consumer group.
+ *
+ * @param string $group
+ * @param string $consumer
+ * @param array $streams
+ * @param int|null $count
+ * @param int|null $block
+ *
+ * @return array The messages delivered to this consumer group (if any).
+ *
+ * @link https://redis.io/commands/xreadgroup
+ * @example
+ *
+ * // Consume messages for 'mygroup', 'consumer1'
+ * $redis->xReadGroup('mygroup', 'consumer1', ['s1' => 0, 's2' => 0]);
+ * // Read a single message as 'consumer2' for up to a second until a message arrives.
+ * $redis->xReadGroup('mygroup', 'consumer2', ['s1' => 0, 's2' => 0], 1, 1000);
+ *
+ */
+ public function xReadGroup($group, $consumer, $streams, $count = null, $block = null)
+ {
+ }
+
+ /**
+ * This is identical to xRange except the results come back in reverse order.
+ * Also note that Redis reverses the order of "start" and "end".
+ *
+ * @param string $stream
+ * @param string $end
+ * @param string $start
+ * @param int $count
+ *
+ * @return array The messages in the range specified
+ *
+ * @link https://redis.io/commands/xrevrange
+ * @example
+ *
+ * $redis->xRevRange('mystream', '+', '-');
+ *
+ */
+ public function xRevRange($stream, $end, $start, $count = null)
+ {
+ }
+
+ /**
+ * Trim the stream length to a given maximum.
+ * If the "approximate" flag is pasesed, Redis will use your size as a hint but only trim trees in whole nodes
+ * (this is more efficient)
+ *
+ * @param string $stream
+ * @param int $maxLen
+ * @param bool $isApproximate
+ *
+ * @return int The number of messages trimed from the stream.
+ *
+ * @link https://redis.io/commands/xtrim
+ * @example
+ *
+ * // Trim to exactly 100 messages
+ * $redis->xTrim('mystream', 100);
+ * // Let Redis approximate the trimming
+ * $redis->xTrim('mystream', 100, true);
+ *
+ */
+ public function xTrim($stream, $maxLen, $isApproximate)
+ {
+ }
+
+ /**
+ * Adds a values to the set value stored at key.
+ *
+ * @param string $key Required key
+ * @param array $values Required values
+ *
+ * @return int|bool The number of elements added to the set.
+ * If this value is already in the set, FALSE is returned
+ *
+ * @link https://redis.io/commands/sadd
+ * @link https://github.com/phpredis/phpredis/commit/3491b188e0022f75b938738f7542603c7aae9077
+ * @since phpredis 2.2.8
+ * @example
+ *
+ * $redis->sAddArray('k', array('v1')); // boolean
+ * $redis->sAddArray('k', array('v1', 'v2', 'v3')); // boolean
+ *
+ */
+ public function sAddArray($key, array $values)
+ {
+ }
+}
+
+class RedisException extends Exception
+{
+}
+
+/**
+ * @mixin \Redis
+ */
+class RedisArray
+{
+ /**
+ * Constructor
+ *
+ * @param string|array $hosts Name of the redis array from redis.ini or array of hosts to construct the array with
+ * @param array $opts Array of options
+ *
+ * @link https://github.com/nicolasff/phpredis/blob/master/arrays.markdown
+ */
+ public function __construct($hosts, array $opts = null)
+ {
+ }
+
+ /**
+ * @return array list of hosts for the selected array
+ */
+ public function _hosts()
+ {
+ }
+
+ /**
+ * @return string the name of the function used to extract key parts during consistent hashing
+ */
+ public function _function()
+ {
+ }
+
+ /**
+ * @param string $key The key for which you want to lookup the host
+ *
+ * @return string the host to be used for a certain key
+ */
+ public function _target($key)
+ {
+ }
+
+ /**
+ * Use this function when a new node is added and keys need to be rehashed.
+ */
+ public function _rehash()
+ {
+ }
+
+ /**
+ * Returns an associative array of strings and integers, with the following keys:
+ * - redis_version
+ * - redis_git_sha1
+ * - redis_git_dirty
+ * - redis_build_id
+ * - redis_mode
+ * - os
+ * - arch_bits
+ * - multiplexing_api
+ * - atomicvar_api
+ * - gcc_version
+ * - process_id
+ * - run_id
+ * - tcp_port
+ * - uptime_in_seconds
+ * - uptime_in_days
+ * - hz
+ * - lru_clock
+ * - executable
+ * - config_file
+ * - connected_clients
+ * - client_longest_output_list
+ * - client_biggest_input_buf
+ * - blocked_clients
+ * - used_memory
+ * - used_memory_human
+ * - used_memory_rss
+ * - used_memory_rss_human
+ * - used_memory_peak
+ * - used_memory_peak_human
+ * - used_memory_peak_perc
+ * - used_memory_peak
+ * - used_memory_overhead
+ * - used_memory_startup
+ * - used_memory_dataset
+ * - used_memory_dataset_perc
+ * - total_system_memory
+ * - total_system_memory_human
+ * - used_memory_lua
+ * - used_memory_lua_human
+ * - maxmemory
+ * - maxmemory_human
+ * - maxmemory_policy
+ * - mem_fragmentation_ratio
+ * - mem_allocator
+ * - active_defrag_running
+ * - lazyfree_pending_objects
+ * - mem_fragmentation_ratio
+ * - loading
+ * - rdb_changes_since_last_save
+ * - rdb_bgsave_in_progress
+ * - rdb_last_save_time
+ * - rdb_last_bgsave_status
+ * - rdb_last_bgsave_time_sec
+ * - rdb_current_bgsave_time_sec
+ * - rdb_last_cow_size
+ * - aof_enabled
+ * - aof_rewrite_in_progress
+ * - aof_rewrite_scheduled
+ * - aof_last_rewrite_time_sec
+ * - aof_current_rewrite_time_sec
+ * - aof_last_bgrewrite_status
+ * - aof_last_write_status
+ * - aof_last_cow_size
+ * - changes_since_last_save
+ * - aof_current_size
+ * - aof_base_size
+ * - aof_pending_rewrite
+ * - aof_buffer_length
+ * - aof_rewrite_buffer_length
+ * - aof_pending_bio_fsync
+ * - aof_delayed_fsync
+ * - loading_start_time
+ * - loading_total_bytes
+ * - loading_loaded_bytes
+ * - loading_loaded_perc
+ * - loading_eta_seconds
+ * - total_connections_received
+ * - total_commands_processed
+ * - instantaneous_ops_per_sec
+ * - total_net_input_bytes
+ * - total_net_output_bytes
+ * - instantaneous_input_kbps
+ * - instantaneous_output_kbps
+ * - rejected_connections
+ * - maxclients
+ * - sync_full
+ * - sync_partial_ok
+ * - sync_partial_err
+ * - expired_keys
+ * - evicted_keys
+ * - keyspace_hits
+ * - keyspace_misses
+ * - pubsub_channels
+ * - pubsub_patterns
+ * - latest_fork_usec
+ * - migrate_cached_sockets
+ * - slave_expires_tracked_keys
+ * - active_defrag_hits
+ * - active_defrag_misses
+ * - active_defrag_key_hits
+ * - active_defrag_key_misses
+ * - role
+ * - master_replid
+ * - master_replid2
+ * - master_repl_offset
+ * - second_repl_offset
+ * - repl_backlog_active
+ * - repl_backlog_size
+ * - repl_backlog_first_byte_offset
+ * - repl_backlog_histlen
+ * - master_host
+ * - master_port
+ * - master_link_status
+ * - master_last_io_seconds_ago
+ * - master_sync_in_progress
+ * - slave_repl_offset
+ * - slave_priority
+ * - slave_read_only
+ * - master_sync_left_bytes
+ * - master_sync_last_io_seconds_ago
+ * - master_link_down_since_seconds
+ * - connected_slaves
+ * - min-slaves-to-write
+ * - min-replicas-to-write
+ * - min_slaves_good_slaves
+ * - used_cpu_sys
+ * - used_cpu_user
+ * - used_cpu_sys_children
+ * - used_cpu_user_children
+ * - cluster_enabled
+ *
+ * @link https://redis.io/commands/info
+ * @return array
+ * @example
+ *
+ * $redis->info();
+ *
+ */
+ public function info() {
+ }
+}
diff --git a/.phan/internal_stubs/memcache.phan_php b/.phan/internal_stubs/memcache.phan_php
new file mode 100644
index 00000000..080129f4
--- /dev/null
+++ b/.phan/internal_stubs/memcache.phan_php
@@ -0,0 +1,460 @@
+
+ * Open memcached server connection
+ * @link https://php.net/manual/en/memcache.connect.php
+ * @param string $host
+ * Point to the host where memcached is listening for connections. This parameter
+ * may also specify other transports like unix:///path/to/memcached.sock
+ * to use UNIX domain sockets, in this case port must also
+ * be set to 0.
+ *
+ * @param int $port [optional]
+ * Point to the port where memcached is listening for connections. Set this
+ * parameter to 0 when using UNIX domain sockets.
+ *
+ *
+ * Please note: port defaults to
+ * {@link https://php.net/manual/ru/memcache.ini.php#ini.memcache.default-port memcache.default_port}
+ * if not specified. For this reason it is wise to specify the port
+ * explicitly in this method call.
+ *
+ * @param int $timeout [optional] Value in seconds which will be used for connecting to the daemon. Think twice before changing the default value of 1 second - you can lose all the advantages of caching if your connection is too slow.
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function connect ($host, $port, $timeout = 1) {}
+
+ /**
+ * (PECL memcache >= 2.0.0)
+ * Add a memcached server to connection pool
+ * @link https://php.net/manual/en/memcache.addserver.php
+ * @param string $host
+ * Point to the host where memcached is listening for connections. This parameter
+ * may also specify other transports like unix:///path/to/memcached.sock
+ * to use UNIX domain sockets, in this case port must also
+ * be set to 0.
+ *
+ * @param int $port [optional]
+ * Point to the port where memcached is listening for connections.
+ * Set this
+ * parameter to 0 when using UNIX domain sockets.
+ *
+ *
+ * Please note: port defaults to
+ * memcache.default_port
+ * if not specified. For this reason it is wise to specify the port
+ * explicitly in this method call.
+ *
+ * @param bool $persistent [optional]
+ * Controls the use of a persistent connection. Default to TRUE.
+ *
+ * @param int $weight [optional]
+ * Number of buckets to create for this server which in turn control its
+ * probability of it being selected. The probability is relative to the
+ * total weight of all servers.
+ *
+ * @param int $timeout [optional]
+ * Value in seconds which will be used for connecting to the daemon. Think
+ * twice before changing the default value of 1 second - you can lose all
+ * the advantages of caching if your connection is too slow.
+ *
+ * @param int $retry_interval [optional]
+ * Controls how often a failed server will be retried, the default value
+ * is 15 seconds. Setting this parameter to -1 disables automatic retry.
+ * Neither this nor the persistent parameter has any
+ * effect when the extension is loaded dynamically via dl.
+ *
+ *
+ * Each failed connection struct has its own timeout and before it has expired
+ * the struct will be skipped when selecting backends to serve a request. Once
+ * expired the connection will be successfully reconnected or marked as failed
+ * for another retry_interval seconds. The typical
+ * effect is that each web server child will retry the connection about every
+ * retry_interval seconds when serving a page.
+ *
+ * @param bool $status [optional]
+ * Controls if the server should be flagged as online. Setting this parameter
+ * to FALSE and retry_interval to -1 allows a failed
+ * server to be kept in the pool so as not to affect the key distribution
+ * algorithm. Requests for this server will then failover or fail immediately
+ * depending on the memcache.allow_failover setting.
+ * Default to TRUE, meaning the server should be considered online.
+ *
+ * @param callable $failure_callback [optional]
+ * Allows the user to specify a callback function to run upon encountering an
+ * error. The callback is run before failover is attempted. The function takes
+ * two parameters, the hostname and port of the failed server.
+ *
+ * @param int $timeoutms [optional]
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function addServer ($host, $port = 11211, $persistent = true, $weight = null, $timeout = 1, $retry_interval = 15, $status = true, callable $failure_callback = null, $timeoutms = null) {}
+
+ /**
+ * (PECL memcache >= 2.1.0)
+ * Changes server parameters and status at runtime
+ * @link https://secure.php.net/manual/en/memcache.setserverparams.php
+ * @param string $host Point to the host where memcached is listening for connections.
+ * Point to the port where memcached is listening for connections.
+ *
+ * @param int $timeout [optional]
+ * Value in seconds which will be used for connecting to the daemon. Think twice before changing the default value of 1 second - you can lose all the advantages of caching if your connection is too slow.
+ *
+ * @param int $retry_interval [optional]
+ * Controls how often a failed server will be retried, the default value
+ * is 15 seconds. Setting this parameter to -1 disables automatic retry.
+ * Neither this nor the persistent parameter has any
+ * effect when the extension is loaded dynamically via {@link https://secure.php.net/manual/en/function.dl.php dl()}.
+ *
+ * @param bool $status [optional]
+ * Controls if the server should be flagged as online. Setting this parameter
+ * to FALSE and retry_interval to -1 allows a failed
+ * server to be kept in the pool so as not to affect the key distribution
+ * algorithm. Requests for this server will then failover or fail immediately
+ * depending on the memcache.allow_failover setting.
+ * Default to TRUE, meaning the server should be considered online.
+ *
+ * @param callable $failure_callback [optional]
+ * Allows the user to specify a callback function to run upon encountering an error. The callback is run before failover is attempted.
+ * The function takes two parameters, the hostname and port of the failed server.
+ *
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function setServerParams ($host, $port = 11211, $timeout = 1, $retry_interval = 15, $status = true, callable $failure_callback = null) {}
+
+ /**
+ *
+ */
+ public function setFailureCallback () {}
+
+ /**
+ * (PECL memcache >= 2.1.0)
+ * Returns server status
+ * @link https://php.net/manual/en/memcache.getserverstatus.php
+ * @param string $host Point to the host where memcached is listening for connections.
+ * @param int $port Point to the port where memcached is listening for connections.
+ * @return int Returns a the servers status. 0 if server is failed, non-zero otherwise
+ */
+ public function getServerStatus ($host, $port = 11211) {}
+
+ /**
+ *
+ */
+ public function findServer () {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Return version of the server
+ * @link https://php.net/manual/en/memcache.getversion.php
+ * @return string|false Returns a string of server version number or FALSE on failure.
+ */
+ public function getVersion () {}
+
+ /**
+ * (PECL memcache >= 2.0.0)
+ * Add an item to the server. If the key already exists, the value will not be added and FALSE will be returned.
+ * @link https://php.net/manual/en/memcache.add.php
+ * @param string $key The key that will be associated with the item.
+ * @param mixed $var The variable to store. Strings and integers are stored as is, other types are stored serialized.
+ * @param int $flag [optional]
+ * Use MEMCACHE_COMPRESSED to store the item
+ * compressed (uses zlib).
+ *
+ * @param int $expire [optional] Expiration time of the item.
+ * If it's equal to zero, the item will never expire.
+ * You can also use Unix timestamp or a number of seconds starting from current time, but in the latter case the number of seconds may not exceed 2592000 (30 days).
+ * @return bool Returns TRUE on success or FALSE on failure. Returns FALSE if such key already exist. For the rest Memcache::add() behaves similarly to Memcache::set().
+ */
+ public function add ($key , $var, $flag = null, $expire = null) {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Stores an item var with key on the memcached server. Parameter expire is expiration time in seconds.
+ * If it's 0, the item never expires (but memcached server doesn't guarantee this item to be stored all the time,
+ * it could be deleted from the cache to make place for other items).
+ * You can use MEMCACHE_COMPRESSED constant as flag value if you want to use on-the-fly compression (uses zlib).
+ * @link https://php.net/manual/en/memcache.set.php
+ * @param string $key The key that will be associated with the item.
+ * @param mixed $var The variable to store. Strings and integers are stored as is, other types are stored serialized.
+ * @param int $flag [optional] Use MEMCACHE_COMPRESSED to store the item compressed (uses zlib).
+ * @param int $expire [optional] Expiration time of the item. If it's equal to zero, the item will never expire. You can also use Unix timestamp or a number of seconds starting from current time, but in the latter case the number of seconds may not exceed 2592000 (30 days).
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function set ($key, $var, $flag = null, $expire = null) {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Replace value of the existing item
+ * @link https://php.net/manual/en/memcache.replace.php
+ * @param string $key The key that will be associated with the item.
+ * @param mixed $var The variable to store. Strings and integers are stored as is, other types are stored serialized.
+ * @param int $flag [optional] Use MEMCACHE_COMPRESSED to store the item compressed (uses zlib).
+ * @param int $expire [optional] Expiration time of the item. If it's equal to zero, the item will never expire. You can also use Unix timestamp or a number of seconds starting from current time, but in the latter case the number of seconds may not exceed 2592000 (30 days).
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function replace ($key, $var, $flag = null, $expire = null) {}
+
+ public function cas () {}
+
+ public function append () {}
+
+ /**
+ * @return string
+ */
+ public function prepend () {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Retrieve item from the server
+ * @link https://php.net/manual/en/memcache.get.php
+ * @param string|array $key
+ * The key or array of keys to fetch.
+ *
+ * @param int|array $flags [optional]
+ * If present, flags fetched along with the values will be written to this parameter. These
+ * flags are the same as the ones given to for example {@link https://php.net/manual/en/memcache.set.php Memcache::set()}.
+ * The lowest byte of the int is reserved for pecl/memcache internal usage (e.g. to indicate
+ * compression and serialization status).
+ *
+ * @return string|array|false
+ * Returns the string associated with the key or
+ * an array of found key-value pairs when key is an {@link https://php.net/manual/en/language.types.array.php array}.
+ * Returns FALSE on failure, key is not found or
+ * key is an empty {@link https://php.net/manual/en/language.types.array.php array}.
+ *
+ */
+ public function get ($key, &$flags = null) {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Delete item from the server
+ * https://secure.php.net/manual/ru/memcache.delete.php
+ * @param $key string The key associated with the item to delete.
+ * @param $timeout int [optional] This deprecated parameter is not supported, and defaults to 0 seconds. Do not use this parameter.
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function delete ($key, $timeout = 0 ) {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Get statistics of the server
+ * @link https://php.net/manual/ru/memcache.getstats.php
+ * @param string $type [optional]
+ * The type of statistics to fetch.
+ * Valid values are {reset, malloc, maps, cachedump, slabs, items, sizes}.
+ * According to the memcached protocol spec these additional arguments "are subject to change for the convenience of memcache developers".
+ * @param int $slabid [optional]
+ * Used in conjunction with type set to
+ * cachedump to identify the slab to dump from. The cachedump
+ * command ties up the server and is strictly to be used for
+ * debugging purposes.
+ *
+ * @param int $limit [optional]
+ * Used in conjunction with type set to cachedump to limit the number of entries to dump.
+ *
+ * @return array|false Returns an associative array of server statistics or FALSE on failure.
+ */
+ public function getStats ($type = null, $slabid = null, $limit = 100) {}
+
+ /**
+ * (PECL memcache >= 2.0.0)
+ * Get statistics from all servers in pool
+ * @link https://php.net/manual/en/memcache.getextendedstats.php
+ * @param string $type [optional] The type of statistics to fetch. Valid values are {reset, malloc, maps, cachedump, slabs, items, sizes}. According to the memcached protocol spec these additional arguments "are subject to change for the convenience of memcache developers".
+ * @param int $slabid [optional]
+ * Used in conjunction with type set to
+ * cachedump to identify the slab to dump from. The cachedump
+ * command ties up the server and is strictly to be used for
+ * debugging purposes.
+ *
+ * @param int $limit Used in conjunction with type set to cachedump to limit the number of entries to dump.
+ * @return array|false Returns a two-dimensional associative array of server statistics or FALSE
+ * Returns a two-dimensional associative array of server statistics or FALSE
+ * on failure.
+ */
+ public function getExtendedStats ($type = null, $slabid = null, $limit = 100) {}
+
+ /**
+ * (PECL memcache >= 2.0.0)
+ * Enable automatic compression of large values
+ * @link https://php.net/manual/en/memcache.setcompressthreshold.php
+ * @param int $thresold Controls the minimum value length before attempting to compress automatically.
+ * @param float $min_saving [optional] Specifies the minimum amount of savings to actually store the value compressed. The supplied value must be between 0 and 1. Default value is 0.2 giving a minimum 20% compression savings.
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function setCompressThreshold ($thresold, $min_saving = 0.2) {}
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Increment item's value
+ * @link https://php.net/manual/en/memcache.increment.php
+ * @param $key string Key of the item to increment.
+ * @param $value int [optional] increment the item by value
+ * @return int|false Returns new items value on success or FALSE on failure.
+ */
+ public function increment ($key, $value = 1) {}
+
+ /**
+ * (PECL memcache >= 0.2.0)
+ * Decrement item's value
+ * @link https://php.net/manual/en/memcache.decrement.php
+ * @param $key string Key of the item do decrement.
+ * @param $value int Decrement the item by value.
+ * @return int|false Returns item's new value on success or FALSE on failure.
+ */
+ public function decrement ($key, $value = 1) {}
+
+ /**
+ * (PECL memcache >= 0.4.0)
+ * Close memcached server connection
+ * @link https://php.net/manual/en/memcache.close.php
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function close () {}
+
+ /**
+ * (PECL memcache >= 1.0.0)
+ * Flush all existing items at the server
+ * @link https://php.net/manual/en/memcache.flush.php
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+ public function flush () {}
+
+}
+
+/**
+ * Represents a connection to a set of memcache servers.
+ * @link https://php.net/manual/en/class.memcache.php
+ */
+class Memcache extends MemcachePool {
+
+
+ /**
+ * (PECL memcache >= 0.4.0)
+ * Open memcached server persistent connection
+ * @link https://php.net/manual/en/memcache.pconnect.php
+ * @param string $host
+ * Point to the host where memcached is listening for connections. This parameter
+ * may also specify other transports like unix:///path/to/memcached.sock
+ * to use UNIX domain sockets, in this case port must also
+ * be set to 0.
+ *
+ * @param int $port [optional]
+ * Point to the port where memcached is listening for connections. Set this
+ * parameter to 0 when using UNIX domain sockets.
+ *
+ * @param int $timeout [optional]
+ * Value in seconds which will be used for connecting to the daemon. Think
+ * twice before changing the default value of 1 second - you can lose all
+ * the advantages of caching if your connection is too slow.
+ *
+ * @return mixed a Memcache object or FALSE on failure.
+ */
+ public function pconnect ($host, $port, $timeout = 1) {}
+}
+
+// string $host [, int $port [, int $timeout ]]
+
+/**
+ * (PECL memcache >= 0.2.0)
+ * Memcache::connect — Open memcached server connection
+ * @link https://php.net/manual/en/memcache.connect.php
+ * @param string $host
+ * Point to the host where memcached is listening for connections.
+ * This parameter may also specify other transports like
+ * unix:///path/to/memcached.sock to use UNIX domain sockets,
+ * in this case port must also be set to 0.
+ *
+ * @param int $port [optional]
+ * Point to the port where memcached is listening for connections.
+ * Set this parameter to 0 when using UNIX domain sockets.
+ * Note: port defaults to memcache.default_port if not specified.
+ * For this reason it is wise to specify the port explicitly in this method call.
+ *
+ * @param int $timeout [optional]
+ * Value in seconds which will be used for connecting to the daemon.
+ *
+ * @return bool Returns TRUE on success or FALSE on failure.
+ */
+function memcache_connect ($host, $port, $timeout = 1) {}
+
+/**
+ * (PECL memcache >= 0.4.0)
+ * Memcache::pconnect — Open memcached server persistent connection
+ *
+ * @link https://php.net/manual/en/memcache.pconnect.php#example-5242
+ * @param $host
+ * @param null $port
+ * @param int $timeout
+ * @return Memcache
+ */
+function memcache_pconnect ($host, $port=null, $timeout=1) {}
+
+function memcache_add_server () {}
+
+function memcache_set_server_params () {}
+
+function memcache_set_failure_callback () {}
+
+function memcache_get_server_status () {}
+
+function memcache_get_version () {}
+
+function memcache_add () {}
+
+function memcache_set () {}
+
+function memcache_replace () {}
+
+function memcache_cas () {}
+
+function memcache_append () {}
+
+function memcache_prepend () {}
+
+function memcache_get () {}
+
+function memcache_delete () {}
+
+/**
+ * (PECL memcache >= 0.2.0)
+ * Turn debug output on/off
+ * @link https://php.net/manual/en/function.memcache-debug.php
+ * @param bool $on_off
+ * Turns debug output on if equals to TRUE.
+ * Turns debug output off if equals to FALSE.
+ *
+ * @return bool TRUE if PHP was built with --enable-debug option, otherwise
+ * returns FALSE.
+ */
+function memcache_debug ($on_off) {}
+
+function memcache_get_stats () {}
+
+function memcache_get_extended_stats () {}
+
+function memcache_set_compress_threshold () {}
+
+function memcache_increment () {}
+
+function memcache_decrement () {}
+
+function memcache_close () {}
+
+function memcache_flush () {}
+
+define ('MEMCACHE_COMPRESSED', 2);
+define ('MEMCACHE_USER1', 65536);
+define ('MEMCACHE_USER2', 131072);
+define ('MEMCACHE_USER3', 262144);
+define ('MEMCACHE_USER4', 524288);
+define ('MEMCACHE_HAVE_SESSION', 1);
+
+// End of memcache v.3.0.8
+?>
diff --git a/.phan/internal_stubs/memcached.phan_php b/.phan/internal_stubs/memcached.phan_php
new file mode 100644
index 00000000..f734bcbe
--- /dev/null
+++ b/.phan/internal_stubs/memcached.phan_php
@@ -0,0 +1,1308 @@
+Enables or disables payload compression. When enabled,
+ * item values longer than a certain threshold (currently 100 bytes) will be
+ * compressed during storage and decompressed during retrieval
+ * transparently.
+ * Type: boolean, default: TRUE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_COMPRESSION = -1001;
+ const OPT_COMPRESSION_TYPE = -1004;
+
+ /**
+ * This can be used to create a "domain" for your item keys. The value
+ * specified here will be prefixed to each of the keys. It cannot be
+ * longer than 128 characters and will reduce the
+ * maximum available key size. The prefix is applied only to the item keys,
+ * not to the server keys.
+ * Type: string, default: "".
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_PREFIX_KEY = -1002;
+
+ /**
+ *
+ * Specifies the serializer to use for serializing non-scalar values.
+ * The valid serializers are Memcached::SERIALIZER_PHP
+ * or Memcached::SERIALIZER_IGBINARY. The latter is
+ * supported only when memcached is configured with
+ * --enable-memcached-igbinary option and the
+ * igbinary extension is loaded.
+ *
+ * Type: integer, default: Memcached::SERIALIZER_PHP.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_SERIALIZER = -1003;
+
+ /**
+ * Indicates whether igbinary serializer support is available.
+ * Type: boolean.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HAVE_IGBINARY = 0;
+
+ /**
+ * Indicates whether JSON serializer support is available.
+ * Type: boolean.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HAVE_JSON = 0;
+ const HAVE_SESSION = 1;
+ const HAVE_SASL = 0;
+
+ /**
+ * Specifies the hashing algorithm used for the item keys. The valid
+ * values are supplied via Memcached::HASH_* constants.
+ * Each hash algorithm has its advantages and its disadvantages. Go with the
+ * default if you don't know or don't care.
+ * Type: integer, default: Memcached::HASH_DEFAULT
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_HASH = 2;
+
+ /**
+ * The default (Jenkins one-at-a-time) item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_DEFAULT = 0;
+
+ /**
+ * MD5 item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_MD5 = 1;
+
+ /**
+ * CRC item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_CRC = 2;
+
+ /**
+ * FNV1_64 item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_FNV1_64 = 3;
+
+ /**
+ * FNV1_64A item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_FNV1A_64 = 4;
+
+ /**
+ * FNV1_32 item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_FNV1_32 = 5;
+
+ /**
+ * FNV1_32A item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_FNV1A_32 = 6;
+
+ /**
+ * Hsieh item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_HSIEH = 7;
+
+ /**
+ * Murmur item key hashing algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const HASH_MURMUR = 8;
+
+ /**
+ * Specifies the method of distributing item keys to the servers.
+ * Currently supported methods are modulo and consistent hashing. Consistent
+ * hashing delivers better distribution and allows servers to be added to
+ * the cluster with minimal cache losses.
+ * Type: integer, default: Memcached::DISTRIBUTION_MODULA.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_DISTRIBUTION = 9;
+
+ /**
+ * Modulo-based key distribution algorithm.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const DISTRIBUTION_MODULA = 0;
+
+ /**
+ * Consistent hashing key distribution algorithm (based on libketama).
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const DISTRIBUTION_CONSISTENT = 1;
+ const DISTRIBUTION_VIRTUAL_BUCKET = 6;
+
+ /**
+ * Enables or disables compatibility with libketama-like behavior. When
+ * enabled, the item key hashing algorithm is set to MD5 and distribution is
+ * set to be weighted consistent hashing distribution. This is useful
+ * because other libketama-based clients (Python, Ruby, etc.) with the same
+ * server configuration will be able to access the keys transparently.
+ *
+ *
+ * It is highly recommended to enable this option if you want to use
+ * consistent hashing, and it may be enabled by default in future
+ * releases.
+ *
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_LIBKETAMA_COMPATIBLE = 16;
+ const OPT_LIBKETAMA_HASH = 17;
+ const OPT_TCP_KEEPALIVE = 32;
+
+ /**
+ * Enables or disables buffered I/O. Enabling buffered I/O causes
+ * storage commands to "buffer" instead of being sent. Any action that
+ * retrieves data causes this buffer to be sent to the remote connection.
+ * Quitting the connection or closing down the connection will also cause
+ * the buffered data to be pushed to the remote connection.
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_BUFFER_WRITES = 10;
+
+ /**
+ * Enable the use of the binary protocol. Please note that you cannot
+ * toggle this option on an open connection.
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_BINARY_PROTOCOL = 18;
+
+ /**
+ * Enables or disables asynchronous I/O. This is the fastest transport
+ * available for storage functions.
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_NO_BLOCK = 0;
+
+ /**
+ * Enables or disables the no-delay feature for connecting sockets (may
+ * be faster in some environments).
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_TCP_NODELAY = 1;
+
+ /**
+ * The maximum socket send buffer in bytes.
+ * Type: integer, default: varies by platform/kernel
+ * configuration.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_SOCKET_SEND_SIZE = 4;
+
+ /**
+ * The maximum socket receive buffer in bytes.
+ * Type: integer, default: varies by platform/kernel
+ * configuration.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_SOCKET_RECV_SIZE = 5;
+
+ /**
+ * In non-blocking mode this set the value of the timeout during socket
+ * connection, in milliseconds.
+ * Type: integer, default: 1000.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_CONNECT_TIMEOUT = 14;
+
+ /**
+ * The amount of time, in seconds, to wait until retrying a failed
+ * connection attempt.
+ * Type: integer, default: 0.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_RETRY_TIMEOUT = 15;
+
+ /**
+ * Socket sending timeout, in microseconds. In cases where you cannot
+ * use non-blocking I/O this will allow you to still have timeouts on the
+ * sending of data.
+ * Type: integer, default: 0.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_SEND_TIMEOUT = 19;
+
+ /**
+ * Socket reading timeout, in microseconds. In cases where you cannot
+ * use non-blocking I/O this will allow you to still have timeouts on the
+ * reading of data.
+ * Type: integer, default: 0.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_RECV_TIMEOUT = 20;
+
+ /**
+ * Timeout for connection polling, in milliseconds.
+ * Type: integer, default: 1000.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_POLL_TIMEOUT = 8;
+
+ /**
+ * Enables or disables caching of DNS lookups.
+ * Type: boolean, default: FALSE.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_CACHE_LOOKUPS = 6;
+
+ /**
+ * Specifies the failure limit for server connection attempts. The
+ * server will be removed after this many continuous connection
+ * failures.
+ * Type: integer, default: 0.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const OPT_SERVER_FAILURE_LIMIT = 21;
+ const OPT_AUTO_EJECT_HOSTS = 28;
+ const OPT_HASH_WITH_PREFIX_KEY = 25;
+ const OPT_NOREPLY = 26;
+ const OPT_SORT_HOSTS = 12;
+ const OPT_VERIFY_KEY = 13;
+ const OPT_USE_UDP = 27;
+ const OPT_NUMBER_OF_REPLICAS = 29;
+ const OPT_RANDOMIZE_REPLICA_READ = 30;
+ const OPT_CORK = 31;
+ const OPT_REMOVE_FAILED_SERVERS = 35;
+ const OPT_DEAD_TIMEOUT = 36;
+ const OPT_SERVER_TIMEOUT_LIMIT = 37;
+ const OPT_MAX = 38;
+ const OPT_IO_BYTES_WATERMARK = 23;
+ const OPT_IO_KEY_PREFETCH = 24;
+ const OPT_IO_MSG_WATERMARK = 22;
+ const OPT_LOAD_FROM_FILE = 34;
+ const OPT_SUPPORT_CAS = 7;
+ const OPT_TCP_KEEPIDLE = 33;
+ const OPT_USER_DATA = 11;
+
+
+ /**
+ * The operation was successful.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_SUCCESS = 0;
+
+ /**
+ * The operation failed in some fashion.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_FAILURE = 1;
+
+ /**
+ * DNS lookup failed.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_HOST_LOOKUP_FAILURE = 2;
+
+ /**
+ * Failed to read network data.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_UNKNOWN_READ_FAILURE = 7;
+
+ /**
+ * Bad command in memcached protocol.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_PROTOCOL_ERROR = 8;
+
+ /**
+ * Error on the client side.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_CLIENT_ERROR = 9;
+
+ /**
+ * Error on the server side.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_SERVER_ERROR = 10;
+
+ /**
+ * Failed to write network data.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_WRITE_FAILURE = 5;
+
+ /**
+ * Failed to do compare-and-swap: item you are trying to store has been
+ * modified since you last fetched it.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_DATA_EXISTS = 12;
+
+ /**
+ * Item was not stored: but not because of an error. This normally
+ * means that either the condition for an "add" or a "replace" command
+ * wasn't met, or that the item is in a delete queue.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_NOTSTORED = 14;
+
+ /**
+ * Item with this key was not found (with "get" operation or "cas"
+ * operations).
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_NOTFOUND = 16;
+
+ /**
+ * Partial network data read error.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_PARTIAL_READ = 18;
+
+ /**
+ * Some errors occurred during multi-get.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_SOME_ERRORS = 19;
+
+ /**
+ * Server list is empty.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_NO_SERVERS = 20;
+
+ /**
+ * End of result set.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_END = 21;
+
+ /**
+ * System error.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_ERRNO = 26;
+
+ /**
+ * The operation was buffered.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_BUFFERED = 32;
+
+ /**
+ * The operation timed out.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_TIMEOUT = 31;
+
+ /**
+ * Bad key.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_BAD_KEY_PROVIDED = 33;
+ const RES_STORED = 15;
+ const RES_DELETED = 22;
+ const RES_STAT = 24;
+ const RES_ITEM = 25;
+ const RES_NOT_SUPPORTED = 28;
+ const RES_FETCH_NOTFINISHED = 30;
+ const RES_SERVER_MARKED_DEAD = 35;
+ const RES_UNKNOWN_STAT_KEY = 36;
+ const RES_INVALID_HOST_PROTOCOL = 34;
+ const RES_MEMORY_ALLOCATION_FAILURE = 17;
+ const RES_E2BIG = 37;
+ const RES_KEY_TOO_BIG = 39;
+ const RES_SERVER_TEMPORARILY_DISABLED = 47;
+ const RES_SERVER_MEMORY_ALLOCATION_FAILURE = 48;
+ const RES_AUTH_PROBLEM = 40;
+ const RES_AUTH_FAILURE = 41;
+ const RES_AUTH_CONTINUE = 42;
+ const RES_CONNECTION_FAILURE = 3;
+ const RES_CONNECTION_BIND_FAILURE = 4;
+ const RES_READ_FAILURE = 6;
+ const RES_DATA_DOES_NOT_EXIST = 13;
+ const RES_VALUE = 23;
+ const RES_FAIL_UNIX_SOCKET = 27;
+ const RES_NO_KEY_PROVIDED = 29;
+ const RES_INVALID_ARGUMENTS = 38;
+ const RES_PARSE_ERROR = 43;
+ const RES_PARSE_USER_ERROR = 44;
+ const RES_DEPRECATED = 45;
+ const RES_IN_PROGRESS = 46;
+ const RES_MAXIMUM_RETURN = 49;
+
+
+
+ /**
+ * Failed to create network socket.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_CONNECTION_SOCKET_CREATE_FAILURE = 11;
+
+ /**
+ * Payload failure: could not compress/decompress or serialize/unserialize the value.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const RES_PAYLOAD_FAILURE = -1001;
+
+ /**
+ * The default PHP serializer.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const SERIALIZER_PHP = 1;
+
+ /**
+ * The igbinary serializer.
+ * Instead of textual representation it stores PHP data structures in a
+ * compact binary form, resulting in space and time gains.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const SERIALIZER_IGBINARY = 2;
+
+ /**
+ * The JSON serializer. Requires PHP 5.2.10+.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const SERIALIZER_JSON = 3;
+ const SERIALIZER_JSON_ARRAY = 4;
+ const COMPRESSION_FASTLZ = 2;
+ const COMPRESSION_ZLIB = 1;
+
+ /**
+ * A flag for Memcached::getMulti and
+ * Memcached::getMultiByKey to ensure that the keys are
+ * returned in the same order as they were requested in. Non-existing keys
+ * get a default value of NULL.
+ * @link https://php.net/manual/en/memcached.constants.php
+ */
+ const GET_PRESERVE_ORDER = 1;
+ const GET_ERROR_RETURN_VALUE = false;
+
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Create a Memcached instance
+ * @link https://php.net/manual/en/memcached.construct.php
+ * @param $persistent_id [optional]
+ * @param $callback [optional]
+ */
+ public function __construct ($persistent_id = '', $on_new_object_cb = null) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Return the result code of the last operation
+ * @link https://php.net/manual/en/memcached.getresultcode.php
+ * @return int Result code of the last Memcached operation.
+ */
+ public function getResultCode () {}
+
+ /**
+ * (PECL memcached >= 1.0.0)
+ * Return the message describing the result of the last operation
+ * @link https://php.net/manual/en/memcached.getresultmessage.php
+ * @return string Message describing the result of the last Memcached operation.
+ */
+ public function getResultMessage () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Retrieve an item
+ * @link https://php.net/manual/en/memcached.get.php
+ * @param string $key
+ * The key of the item to retrieve.
+ *
+ * @param callable $cache_cb [optional]
+ * Read-through caching callback or NULL.
+ *
+ * @param int $flags [optional]
+ * The flags for the get operation.
+ *
+ * @return mixed the value stored in the cache or FALSE otherwise.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function get ($key, callable $cache_cb = null, $flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Retrieve an item from a specific server
+ * @link https://php.net/manual/en/memcached.getbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key of the item to fetch.
+ *
+ * @param callable $cache_cb [optional]
+ * Read-through caching callback or NULL
+ *
+ * @param int $flags [optional]
+ * The flags for the get operation.
+ *
+ * @return mixed the value stored in the cache or FALSE otherwise.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function getByKey ($server_key, $key, callable $cache_cb = null, $flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Retrieve multiple items
+ * @link https://php.net/manual/en/memcached.getmulti.php
+ * @param array $keys
+ * Array of keys to retrieve.
+ *
+ * @param int $flags [optional]
+ * The flags for the get operation.
+ *
+ * @return mixed the array of found items or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function getMulti (array $keys, $flags = null) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Retrieve multiple items from a specific server
+ * @link https://php.net/manual/en/memcached.getmultibykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param array $keys
+ * Array of keys to retrieve.
+ *
+ * @param int $flags [optional]
+ * The flags for the get operation.
+ *
+ * @return array|false the array of found items or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function getMultiByKey ($server_key, array $keys, $flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Request multiple items
+ * @link https://php.net/manual/en/memcached.getdelayed.php
+ * @param array $keys
+ * Array of keys to request.
+ *
+ * @param bool $with_cas [optional]
+ * Whether to request CAS token values also.
+ *
+ * @param callable $value_cb [optional]
+ * The result callback or NULL.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function getDelayed (array $keys, $with_cas = null, callable $value_cb = null) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Request multiple items from a specific server
+ * @link https://php.net/manual/en/memcached.getdelayedbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param array $keys
+ * Array of keys to request.
+ *
+ * @param bool $with_cas [optional]
+ * Whether to request CAS token values also.
+ *
+ * @param callable $value_cb [optional]
+ * The result callback or NULL.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function getDelayedByKey ($server_key, array $keys, $with_cas = null, callable $value_cb = null) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Fetch the next result
+ * @link https://php.net/manual/en/memcached.fetch.php
+ * @return array|false the next result or FALSE otherwise.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_END if result set is exhausted.
+ */
+ public function fetch () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Fetch all the remaining results
+ * @link https://php.net/manual/en/memcached.fetchall.php
+ * @return array|false the results or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function fetchAll () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Store an item
+ * @link https://php.net/manual/en/memcached.set.php
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function set ($key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Store an item on a specific server
+ * @link https://php.net/manual/en/memcached.setbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function setByKey ($server_key, $key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Set a new expiration on an item
+ * @link https://php.net/manual/en/memcached.touch.php
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param int $expiration
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function touch ($key, $expiration = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Set a new expiration on an item on a specific server
+ * @link https://php.net/manual/en/memcached.touchbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param int $expiration
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function touchByKey ($server_key, $key, $expiration) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Store multiple items
+ * @link https://php.net/manual/en/memcached.setmulti.php
+ * @param array $items
+ * An array of key/value pairs to store on the server.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function setMulti (array $items, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Store multiple items on a specific server
+ * @link https://php.net/manual/en/memcached.setmultibykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param array $items
+ * An array of key/value pairs to store on the server.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function setMultiByKey ($server_key, array $items, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Compare and swap an item
+ * @link https://php.net/manual/en/memcached.cas.php
+ * @param float $cas_token
+ * Unique value associated with the existing item. Generated by memcache.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_DATA_EXISTS if the item you are trying
+ * to store has been modified since you last fetched it.
+ */
+ public function cas ($cas_token, $key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Compare and swap an item on a specific server
+ * @link https://php.net/manual/en/memcached.casbykey.php
+ * @param float $cas_token
+ * Unique value associated with the existing item. Generated by memcache.
+ *
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_DATA_EXISTS if the item you are trying
+ * to store has been modified since you last fetched it.
+ */
+ public function casByKey ($cas_token, $server_key, $key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Add an item under a new key
+ * @link https://php.net/manual/en/memcached.add.php
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key already exists.
+ */
+ public function add ($key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Add an item under a new key on a specific server
+ * @link https://php.net/manual/en/memcached.addbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key already exists.
+ */
+ public function addByKey ($server_key, $key, $value, $expiration = 0, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Append data to an existing item
+ * @link https://php.net/manual/en/memcached.append.php
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param string $value
+ * The string to append.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function append ($key, $value) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Append data to an existing item on a specific server
+ * @link https://php.net/manual/en/memcached.appendbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param string $value
+ * The string to append.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function appendByKey ($server_key, $key, $value) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Prepend data to an existing item
+ * @link https://php.net/manual/en/memcached.prepend.php
+ * @param string $key
+ * The key of the item to prepend the data to.
+ *
+ * @param string $value
+ * The string to prepend.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function prepend ($key, $value) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Prepend data to an existing item on a specific server
+ * @link https://php.net/manual/en/memcached.prependbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key of the item to prepend the data to.
+ *
+ * @param string $value
+ * The string to prepend.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function prependByKey ($server_key, $key, $value) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Replace the item under an existing key
+ * @link https://php.net/manual/en/memcached.replace.php
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function replace ($key, $value, $expiration = null, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Replace the item under an existing key on a specific server
+ * @link https://php.net/manual/en/memcached.replacebykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key under which to store the value.
+ *
+ * @param mixed $value
+ * The value to store.
+ *
+ * @param int $expiration [optional]
+ * The expiration time, defaults to 0. See Expiration Times for more info.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTSTORED if the key does not exist.
+ */
+ public function replaceByKey ($server_key, $key, $value, $expiration = null, $udf_flags = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Delete an item
+ * @link https://php.net/manual/en/memcached.delete.php
+ * @param string $key
+ * The key to be deleted.
+ *
+ * @param int $time [optional]
+ * The amount of time the server will wait to delete the item.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function delete ($key, $time = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Delete multiple items
+ * @link https://php.net/manual/en/memcached.deletemulti.php
+ * @param array $keys
+ * The keys to be deleted.
+ *
+ * @param int $time [optional]
+ * The amount of time the server will wait to delete the items.
+ *
+ * @return array Returns array indexed by keys and where values are indicating whether operation succeeded or not.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function deleteMulti (array $keys, $time = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Delete an item from a specific server
+ * @link https://php.net/manual/en/memcached.deletebykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key to be deleted.
+ *
+ * @param int $time [optional]
+ * The amount of time the server will wait to delete the item.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function deleteByKey ($server_key, $key, $time = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Delete multiple items from a specific server
+ * @link https://php.net/manual/en/memcached.deletemultibykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param array $keys
+ * The keys to be deleted.
+ *
+ * @param int $time [optional]
+ * The amount of time the server will wait to delete the items.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * The Memcached::getResultCode will return
+ * Memcached::RES_NOTFOUND if the key does not exist.
+ */
+ public function deleteMultiByKey ($server_key, array $keys, $time = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Increment numeric item's value
+ * @link https://php.net/manual/en/memcached.increment.php
+ * @param string $key
+ * The key of the item to increment.
+ *
+ * @param int $offset [optional]
+ * The amount by which to increment the item's value.
+ *
+ * @param int $initial_value [optional]
+ * The value to set the item to if it doesn't currently exist.
+ *
+ * @param int $expiry [optional]
+ * The expiry time to set on the item.
+ *
+ * @return int|false new item's value on success or FALSE on failure.
+ */
+ public function increment ($key, $offset = 1, $initial_value = 0, $expiry = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Decrement numeric item's value
+ * @link https://php.net/manual/en/memcached.decrement.php
+ * @param string $key
+ * The key of the item to decrement.
+ *
+ * @param int $offset [optional]
+ * The amount by which to decrement the item's value.
+ *
+ * @param int $initial_value [optional]
+ * The value to set the item to if it doesn't currently exist.
+ *
+ * @param int $expiry [optional]
+ * The expiry time to set on the item.
+ *
+ * @return int|false item's new value on success or FALSE on failure.
+ */
+ public function decrement ($key, $offset = 1, $initial_value = 0, $expiry = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Increment numeric item's value, stored on a specific server
+ * @link https://php.net/manual/en/memcached.incrementbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key of the item to increment.
+ *
+ * @param int $offset [optional]
+ * The amount by which to increment the item's value.
+ *
+ * @param int $initial_value [optional]
+ * The value to set the item to if it doesn't currently exist.
+ *
+ * @param int $expiry [optional]
+ * The expiry time to set on the item.
+ *
+ * @return int|false new item's value on success or FALSE on failure.
+ */
+ public function incrementByKey ($server_key, $key, $offset = 1, $initial_value = 0, $expiry = 0) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Decrement numeric item's value, stored on a specific server
+ * @link https://php.net/manual/en/memcached.decrementbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @param string $key
+ * The key of the item to decrement.
+ *
+ * @param int $offset [optional]
+ * The amount by which to decrement the item's value.
+ *
+ * @param int $initial_value [optional]
+ * The value to set the item to if it doesn't currently exist.
+ *
+ * @param int $expiry [optional]
+ * The expiry time to set on the item.
+ *
+ * @return int|false item's new value on success or FALSE on failure.
+ */
+ public function decrementByKey ($server_key, $key, $offset = 1, $initial_value = 0, $expiry = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Add a server to the server pool
+ * @link https://php.net/manual/en/memcached.addserver.php
+ * @param string $host
+ * The hostname of the memcache server. If the hostname is invalid, data-related
+ * operations will set
+ * Memcached::RES_HOST_LOOKUP_FAILURE result code.
+ *
+ * @param int $port
+ * The port on which memcache is running. Usually, this is
+ * 11211.
+ *
+ * @param int $weight [optional]
+ * The weight of the server relative to the total weight of all the
+ * servers in the pool. This controls the probability of the server being
+ * selected for operations. This is used only with consistent distribution
+ * option and usually corresponds to the amount of memory available to
+ * memcache on that server.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function addServer ($host, $port, $weight = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.1)
+ * Add multiple servers to the server pool
+ * @link https://php.net/manual/en/memcached.addservers.php
+ * @param array $servers
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function addServers (array $servers) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Get the list of the servers in the pool
+ * @link https://php.net/manual/en/memcached.getserverlist.php
+ * @return array The list of all servers in the server pool.
+ */
+ public function getServerList () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Map a key to a server
+ * @link https://php.net/manual/en/memcached.getserverbykey.php
+ * @param string $server_key
+ * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations.
+ *
+ * @return array an array containing three keys of host,
+ * port, and weight on success or FALSE
+ * on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function getServerByKey ($server_key) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Clears all servers from the server list
+ * @link https://php.net/manual/en/memcached.resetserverlist.php
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function resetServerList () {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Close any open connections
+ * @link https://php.net/manual/en/memcached.quit.php
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function quit () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Get server pool statistics
+ * @link https://php.net/manual/en/memcached.getstats.php
+ * @param string $type
+ * @return array Array of server statistics, one entry per server.
+ */
+ public function getStats ($type = null) {}
+
+ /**
+ * (PECL memcached >= 0.1.5)
+ * Get server pool version info
+ * @link https://php.net/manual/en/memcached.getversion.php
+ * @return array Array of server versions, one entry per server.
+ */
+ public function getVersion () {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Gets the keys stored on all the servers
+ * @link https://php.net/manual/en/memcached.getallkeys.php
+ * @return array|false the keys stored on all the servers on success or FALSE on failure.
+ */
+ public function getAllKeys () {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Invalidate all items in the cache
+ * @link https://php.net/manual/en/memcached.flush.php
+ * @param int $delay [optional]
+ * Numer of seconds to wait before invalidating the items.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ * Use Memcached::getResultCode if necessary.
+ */
+ public function flush ($delay = 0) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Retrieve a Memcached option value
+ * @link https://php.net/manual/en/memcached.getoption.php
+ * @param int $option
+ * One of the Memcached::OPT_* constants.
+ *
+ * @return mixed the value of the requested option, or FALSE on
+ * error.
+ */
+ public function getOption ($option) {}
+
+ /**
+ * (PECL memcached >= 0.1.0)
+ * Set a Memcached option
+ * @link https://php.net/manual/en/memcached.setoption.php
+ * @param int $option
+ * @param mixed $value
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function setOption ($option, $value) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Set Memcached options
+ * @link https://php.net/manual/en/memcached.setoptions.php
+ * @param array $options
+ * An associative array of options where the key is the option to set and
+ * the value is the new value for the option.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function setOptions (array $options) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Set the credentials to use for authentication
+ * @link https://secure.php.net/manual/en/memcached.setsaslauthdata.php
+ * @param string $username
+ * The username to use for authentication.
+ *
+ * @param string $password
+ * The password to use for authentication.
+ *
+ * @return bool TRUE on success or FALSE on failure.
+ */
+ public function setSaslAuthData (string $username , string $password) {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Check if a persitent connection to memcache is being used
+ * @link https://php.net/manual/en/memcached.ispersistent.php
+ * @return bool true if Memcache instance uses a persistent connection, false otherwise.
+ */
+ public function isPersistent () {}
+
+ /**
+ * (PECL memcached >= 2.0.0)
+ * Check if the instance was recently created
+ * @link https://php.net/manual/en/memcached.ispristine.php
+ * @return bool the true if instance is recently created, false otherwise.
+ */
+ public function isPristine () {}
+
+ public function flushBuffers () {}
+
+ public function setEncodingKey ( $key ) {}
+
+ public function getLastDisconnectedServer () {}
+
+ public function getLastErrorErrno () {}
+
+ public function getLastErrorCode () {}
+
+ public function getLastErrorMessage () {}
+
+ public function setBucket (array $host_map, array $forward_map, $replicas) {}
+
+}
+
+/**
+ * @link https://php.net/manual/en/class.memcachedexception.php
+ */
+class MemcachedException extends RuntimeException {
+
+}
+// End of memcached v.3.0.4
+?>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf464b04..2e67e470 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,1027 @@
+# v1.7.23
+## 09/29/2021
+
+1. [](#new)
+ * Added method `Pages::referrerRoute()` to get the referrer route and language
+ * Added true unique `Utils::uniqueId()` / `{{ unique_id() }}` utilities with length, prefix, and suffix support
+2. [](#improved)
+ * Replaced GPL `SVG-Sanitizer` with MIT licensed `DOM-Sanitizer`
+ * `Uri::referrer()` now accepts third parameter, if set to `true`, it returns route without base or language code [#3411](https://github.com/getgrav/grav/issues/3411)
+ * Updated vendor libs with latest
+ * Updated with latest language strings via Crowdin.com
+3. [](#bugfix)
+ * Fixed `Folder::move()` throwing an error when target folder is changed by only appending characters to the end [#3445](https://github.com/getgrav/grav/issues/3445)
+ * Fixed some phpstan issues (all code back to level 1, Framework level 3)
+ * Fixed form reset causing image uploads to fail when using Flex
+
+# v1.7.22
+## 09/16/2021
+
+1. [](#new)
+ * Register plugin autoloaders into plugin objects
+2. [](#improved)
+ * Improve Twig 2 compatibility
+ * Update to customized version of Twig DeferredExtension (Twig 1/2 compatible)
+3. [](#bugfix)
+ * Fixed conflicting `$_original` variable in `Flex Pages`
+
+# v1.7.21
+## 09/14/2021
+
+1. [](#new)
+ * Added `|yaml` filter to convert input to YAML
+ * Added `route` and `request` to `onPageNotFound` event
+ * Added file upload/remove support for `Flex Forms`
+ * Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints
+ * Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()`
+ * Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages
+2. [](#improved)
+ * Use a simplified text-based `cron` field for scheduler
+ * Add timestamp to logging output of scheduler jobs to see when they ran
+3. [](#bugfix)
+ * Fixed escaping in PageIndex::getLevelListing()
+ * Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433)
+ * Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432)
+ * Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439)
+ * Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie
+
+# v1.7.20
+## 09/01/2021
+
+2. [](#improved)
+ * Added support for `task` and `action` inside JSON request body
+
+# v1.7.19
+## 08/31/2021
+
+1. [](#new)
+ * Include active form and request in `onPageTask` and `onPageAction` events (defaults to `null`)
+ * Added `UserObject::$authorizeCallable` to allow `$user->authorize()` customization
+2. [](#improved)
+ * Added meta support for `UploadedFile` class
+ * Added support for multiple mime-types per file extension [#3422](https://github.com/getgrav/grav/issues/3422)
+ * Added `setCurrent()` method to Page Collection [#3398](https://github.com/getgrav/grav/pull/3398)
+ * Initialize `$grav['uri']` before session
+3. [](#bugfix)
+ * Fixed `Warning: Undefined array key "SERVER_SOFTWARE" in index.php` [#3408](https://github.com/getgrav/grav/issues/3408)
+ * Fixed error in `loadDirectoryConfig()` if configuration hasn't been saved [#3409](https://github.com/getgrav/grav/issues/3409)
+ * Fixed GPM not using non-standard cache path [#3410](https://github.com/getgrav/grav/issues/3410)
+ * Fixed broken `environment://` stream when it doesn't have configuration
+ * Fixed `Flex Object` missing key field value when using `FolderStorage`
+ * Fixed broken Twig try tag when catch has not been defined or is empty
+ * Fixed `FlexForm` serialization
+ * Fixed form validation for numeric values in PHP 8
+ * Fixed `flex-options@` in blueprints duplicating items in array
+ * Fixed wrong form issue with flex objects after cache clear
+ * Fixed Flex object types not implementing `MediaInterface`
+ * Fixed issue with `svgImageFunction()` that was causing broken output
+
+# v1.7.18
+## 07/19/2021
+
+1. [](#improved)
+ * Added support for loading Flex Directory configuration from main configuration
+ * Move SVGs that cannot be sanitized to quarantine folder under `log://quarantine`
+ * Added support for CloudFlare-forwarded client IP in the `URI::ip()` method
+1. [](#bugfix)
+ * Fixed error when using Flex `SimpleStorage` with no entries
+ * Fixed page search to include slug field [#3316](https://github.com/getgrav/grav/issues/3316)
+ * Fixed Admin becoming unusable when GPM cannot be reached [#3383](https://github.com/getgrav/grav/issues/3383)
+ * Fixed `Failed to save entry: Forbidden` when moving a page to a visible page [#3389](https://github.com/getgrav/grav/issues/3389)
+ * Better support for Symfony local server on linux [#3400](https://github.com/getgrav/grav/pull/3400)
+ * Fixed `open_basedir()` error with some forms
+
+# v1.7.17
+## 06/15/2021
+
+1. [](#new)
+ * Interface `FlexDirectoryInterface` now extends `FlexAuthorizeInterface`
+1. [](#improved)
+ * Allow to unset an asset attribute by specifying null (ie, `'defer': null`)
+ * Support specifying custom attributes to assets in a collection [Read more](https://learn.getgrav.org/17/themes/asset-manager#collections-with-attributes?target=_blank) [#3358](https://github.com/getgrav/grav/issues/3358)
+ * File `frontmatter.yaml` isn't part of media, ignore it
+ * Switched default `JQuery` collection to use 3.x rather than 2.x
+1. [](#bugfix)
+ * Fixed missing styles when CSS/JS Pipeline is used and `asset://` folder is missing
+ * Fixed permission check when moving a page [#3382](https://github.com/getgrav/grav/issues/3382)
+
+# v1.7.16
+## 06/02/2021
+
+1. [](#new)
+ * Added 'addFrame()' method to ImageMedium [#3323](https://github.com/getgrav/grav/pull/3323)
+1. [](#improved)
+ * Set `cache.clear_images_by_default` to `false` by default
+ * Improve error on bad nested form data [#3364](https://github.com/getgrav/grav/issues/3364)
+1. [](#bugfix)
+ * Improve Plugin and Theme initialization to fix PHP8 bug [#3368](https://github.com/getgrav/grav/issues/3368)
+ * Fixed `pathinfo()` twig filter in PHP7
+ * Fixed the first visible child page getting ordering number `999999.` [#3365](https://github.com/getgrav/grav/issues/3365)
+ * Fixed flex pages search using only folder name [#3316](https://github.com/getgrav/grav/issues/3316)
+ * Fixed flex pages using wrong type in `onBlueprintCreated` event [#3157](https://github.com/getgrav/grav/issues/3157)
+ * Fixed wrong SRI paths invoked when Grav instance as a sub folder [#3358](https://github.com/getgrav/grav/issues/3358)
+ * Fixed SRI trying to calculate remote assets, only ever set integrity for local files. Use the SRI provided by the remote source and manually add it in the `addJs/addCss` call for remote support. [#3358](https://github.com/getgrav/grav/issues/3358)
+ * Fix for weird regex issue with latest PHP versions on Intel Macs causing params to not parse properly in URI object
+
+# v1.7.15
+## 05/19/2021
+
+1. [](#improved)
+ * Allow optional start date in page collections [#3350](https://github.com/getgrav/grav/pull/3350)
+ * Added `page` and `output` properties to `onOutputGenerated` and `onOutputRendered` events
+1. [](#bugfix)
+ * Fixed twig deprecated TwigFilter messages [#3348](https://github.com/getgrav/grav/issues/3348)
+ * Fixed fatal error with some markdown links [getgrav/grav-premium-issues#95](https://github.com/getgrav/grav-premium-issues/issues/95)
+ * Fixed markdown media operations not working when using `image://` stream [#3333](https://github.com/getgrav/grav/issues/3333) [#3349](https://github.com/getgrav/grav/issues/3349)
+ * Fixed copying page without changing the slug [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2139)
+ * Fixed missing and commonly used methods when using `system.twig.undefined_functions = false` [getgrav/grav-plugin-admin#2138](https://github.com/getgrav/grav-plugin-admin/issues/2138)
+ * Fixed uploading images into Flex Object if field destination is not set
+
+# v1.7.14
+## 04/29/2021
+
+1. [](#new)
+ * Added `MediaUploadTrait::checkFileMetadata()` method
+1. [](#improved)
+ * Updating a theme should always keep the custom files [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135)
+1. [](#bugfix)
+ * Fixed broken numeric language codes in Flex Pages [#3332](https://github.com/getgrav/grav/issues/3332)
+ * Fixed broken `exif_imagetype()` twig function
+
+# v1.7.13
+## 04/23/2021
+
+1. [](#new)
+ * Added support for getting translated collection of Flex Pages using `$collection->withTranslated('de')`
+1. [](#improved)
+ * Moved `gregwar/Image` and `gregwar/Cache` in-house to official `getgrav/Image` and `getgrav/Cache` packagist packages. This will help environments with very strict proxy setups that don't allow VCS setup. [#3289](https://github.com/getgrav/grav/issues/3289)
+ * Improved XSS Invalid Protocol detection regex [#3298](https://github.com/getgrav/grav/issues/3298)
+ * Added support for user provided folder in Flex `$page->copy()`
+1. [](#bugfix)
+ * Fixed `The "Grav/Common/Twig/TwigExtension" extension is not enabled` when using markdown twig tag [#3317](https://github.com/getgrav/grav/issues/3317)
+ * Fixed text field maxlength validation newline issue [#3324](https://github.com/getgrav/grav/issues/3324)
+ * Fixed a bug in Flex Object `refresh()` method
+
+# v1.7.12
+## 04/15/2021
+
+1. [](#improved)
+ * Improve JSON support for the request
+1. [](#bugfix)
+ * Fixed absolute path support for Windows [#3297](https://github.com/getgrav/grav/issues/3297)
+ * Fixed adding tags in admin after upgrading Grav [#3315](https://github.com/getgrav/grav/issues/3315)
+
+# v1.7.11
+## 04/13/2021
+
+1. [](#new)
+ * Added configuration options to allow PHP methods to be used in Twig functions (`system.twig.safe_functions`) and filters (`system.twig.safe_filters`)
+ * Deprecated using PHP methods in Twig without them being in the safe lists
+ * Prevent dangerous PHP methods from being used as Twig functions and filters
+ * Restrict filesystem Twig functions to accept only local filesystem and grav streams
+1. [](#improved)
+ * Better GPM detection of unauthorized installations
+1. [](#bugfix)
+ * **IMPORTANT** Fixed security vulnerability with Twig allowing dangerous PHP functions by default [GHSA-g8r4-p96j-xfxc](https://github.com/getgrav/grav/security/advisories/GHSA-g8r4-p96j-xfxc)
+ * Fixed nxinx appending repeating `?_url=` in some redirects
+ * Fixed deleting page with language code not removing the folder if it was the last language [#3305](https://github.com/getgrav/grav/issues/3305)
+ * Fixed fatal error when using markdown links with `image://` stream [#3285](https://github.com/getgrav/grav/issues/3285)
+ * Fixed `system.languages.session_store_active` not having any effect [#3269](https://github.com/getgrav/grav/issues/3269)
+ * Fixed fatal error if `system.pages.types` is not an array [#2984](https://github.com/getgrav/grav/issues/2984)
+
+# v1.7.10
+## 04/06/2021
+
+1. [](#new)
+ * Added initial support for running Grav library from outside the webroot [#3297](https://github.com/getgrav/grav/issues/3297)
+1. [](#improved)
+ * Improved password handling when saving a user
+1. [](#bugfix)
+ * Ignore errors when using `set_time_limit` in `Archiver` and `GPM\Response` classes [#3023](https://github.com/getgrav/grav/issues/3023)
+ * Fixed `Folder::move()` deleting the folder if you move folder into itself, created empty file instead
+ * Fixed moving `Flex Page` to itself causing the page to be lost [#3227](https://github.com/getgrav/grav/issues/3227)
+ * Fixed `PageStorage` from detecting files as pages
+ * Fixed `UserIndex` not implementing `UserCollectionInterface`
+ * Fixed missing `onAdminAfterDelete` event call in `Flex Pages`
+ * Fixed system templates not getting scanned [#3296](https://github.com/getgrav/grav/issues/3296)
+ * Fixed incorrect routing if url path looks like a domain name [#2184](https://github.com/getgrav/grav/issues/2184)
+
+# v1.7.9
+## 03/19/2021
+
+1. [](#new)
+ * Added `Media::hide()` method to hide files from media
+ * Added `Utils::getPathFromToken()` method which works also with `Flex Objects`
+ * Added `FlexMediaTrait::getMediaField()`, which can be used to access custom media set in the blueprint fields
+ * Added `FlexMediaTrait::getFieldSettings()`, which can be used to get media field settings
+1. [](#improved)
+ * Method `Utils::getPagePathFromToken()` now calls the more generic `Utils::getPathFromToken()`
+ * Updated `SECURITY.md` to use security@getgrav.org
+1. [](#bugfix)
+ * Fixed broken media upload in `Flex` with `@self/path`, `@page` and `@theme` destinations [#3275](https://github.com/getgrav/grav/issues/3275)
+ * Fixed media fields excluding newly deleted files before saving the object
+ * Fixed method `$pages->find()` should never redirect [#3266](https://github.com/getgrav/grav/pull/3266)
+ * Fixed `Page::activeChild()` throwing an error [#3276](https://github.com/getgrav/grav/issues/3276)
+ * Fixed `Flex Page` CRUD ACL when creating a new page (needs Flex Objects plugin update) [grav-plugin-flex-objects#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115)
+ * Fixed the list of pages not showing up in admin [#3280](https://github.com/getgrav/grav/issues/3280)
+ * Fixed text field min/max validation for UTF8 characters [#3281](https://github.com/getgrav/grav/issues/3281)
+ * Fixed redirects using wrong redirect code
+
+# v1.7.8
+## 03/17/2021
+
+1. [](#new)
+ * Added `ControllerResponseTrait::createDownloadResponse()` method
+ * Added full blueprint support to theme if you move existing files in `blueprints/` to `blueprints/pages/` folder [#3255](https://github.com/getgrav/grav/issues/3255)
+ * Added support for `Theme::getFormFieldTypes()` just like in plugins
+1. [](#improved)
+ * Optimized `Flex Pages` for speed
+ * Optimized saving visible/ordered pages when there are a lot of siblings [#3231](https://github.com/getgrav/grav/issues/3231)
+ * Clearing cache now deletes all clockwork files
+ * Improved `system.pages.redirect_default_route` and `system.pages.redirect_trailing_slash` configuration options to accept redirect code
+1. [](#bugfix)
+ * Fixed clockwork error when clearing cache
+ * Fixed missing method `translated()` in `Flex Pages`
+ * Fixed missing `Flex Pages` in site if multi-language support has been enabled
+ * Fixed Grav using blueprints and form fields from disabled plugins
+ * Fixed `FlexIndex::sortBy(['key' => 'ASC'])` having no effect
+ * Fixed default Flex Pages collection ordering to order by filesystem path
+ * Fixed disappearing pages on save if `pages://` stream resolves to multiple folders where the preferred folder doesn't exist
+ * Fixed Markdown image attribute `loading` [#3251](https://github.com/getgrav/grav/pull/3251)
+ * Fixed `Uri::isValidExtension()` returning false positives
+ * Fixed `page.html` returning duplicated content with `system.pages.redirect_default_route` turned on [#3130](https://github.com/getgrav/grav/issues/3130)
+ * Fixed site redirect with redirect code failing when redirecting to sub-pages [#3035](https://github.com/getgrav/grav/pull/3035/files)
+ * Fixed `Uncaught ValueError: Path cannot be empty` when failing to upload a file [#3265](https://github.com/getgrav/grav/issues/3265)
+ * Fixed `Path cannot be empty` when viewing non-existent log file [#3270](https://github.com/getgrav/grav/issues/3270)
+ * Fixed `onAdminSave` original page having empty header [#3259](https://github.com/getgrav/grav/issues/3259)
+
+# v1.7.7
+## 02/23/2021
+
+1. [](#new)
+ * Added `Utils::arrayToQueryParams()` to convert an array into query params
+1. [](#improved)
+ * Added original image support for all flex objects and media fields
+ * Improved `Pagination` class to allow custom pagination query parameter
+1. [](#bugfix)
+ * Fixed avatar of the user not being saved [grav-plugin-flex-objects#111](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/111)
+ * Replaced special space character with regular space in `system/blueprints/user/account_new.yaml`
+
+# v1.7.6
+## 02/17/2021
+
+1. [](#new)
+ * Added `Medium::attribute()` to pass arbitrary attributes [#3065](https://github.com/getgrav/grav/pull/3065)
+ * Added `Plugins::getPlugins()` and `Plugins::getPlugin($name)` to make it easier to access plugin instances [#2277](https://github.com/getgrav/grav/pull/2277)
+ * Added `regex_match` and `regex_split` twig functions [#2788](https://github.com/getgrav/grav/pull/2788)
+ * Updated all languages from [Crowdin](https://crowdin.com/project/grav-core) - Please update any translations here
+1. [](#improved)
+ * Added abstract `FlexObject`, `FlexCollection` and `FlexIndex` classes to `\Grav\Common\Flex` namespace (extend those instead of Framework or Generic classes)
+ * Updated bundled `composer.phar` binary to latest version `2.0.9`
+ * Improved session fixation handling in PHP 7.4+ (cannot fix it in PHP 7.3 due to PHP bug)
+ * Added optional password/database attributes for redis in `system.yaml`
+ * Added ability to filter enabled or disabled with bin/gpm index [#3187](https://github.com/getgrav/grav/pull/3187)
+ * Added `$grav->getVersion()` or `grav.version` in twig to get the current Grav version [#3142](https://github.com/getgrav/grav/issues/3142)
+ * Added second parameter to `$blueprint->flattenData()` to include every field, including those which have no data
+ * Added support for setting session domain [#2040](https://github.com/getgrav/grav/pull/2040)
+ * Better support inheriting languages when using child themes [#3226](https://github.com/getgrav/grav/pull/3226)
+ * Added option for `FlexForm` constructor to reset the form
+1. [](#bugfix)
+ * Fixed issue with `content-security-policy` not being properly supported with `http-equiv` + support single quotes
+ * Fixed CLI progressbar in `backup` and `security` commands to use styled output [#3198](https://github.com/getgrav/grav/issues/3198)
+ * Fixed page save failing because of uploaded images [#3191](https://github.com/getgrav/grav/issues/3191)
+ * Fixed `Flex Pages` using only default language in frontend [#106](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/106)
+ * Fixed empty `route()` and `raw_route()` when getting translated pages [#3184](https://github.com/getgrav/grav/pull/3184)
+ * Fixed error on `bin/gpm plugin uninstall` [#3207](https://github.com/getgrav/grav/issues/3207)
+ * Fixed broken min/max validation for field `type: int`
+ * Fixed lowering uppercase characters in usernames when saving from frontend [#2565](https://github.com/getgrav/grav/pull/2565)
+ * Fixed save error when editing accounts that have been created with capital letters in their username [#3211](https://github.com/getgrav/grav/issues/3211)
+ * Fixed renaming flex objects key when using file storage
+ * Fixed wrong values in Admin pages list [#3214](https://github.com/getgrav/grav/issues/3214)
+ * Fixed pipelined asset using different hash when extra asset is added to before/after position [#2781](https://github.com/getgrav/grav/issues/2781)
+ * Fixed trailing slash redirect to only apply to GET/HEAD requests and use 301 status code [#3127](https://github.com/getgrav/grav/issues/3127)
+ * Fixed root page to always contain trailing slash [#3127](https://github.com/getgrav/grav/issues/3127)
+ * Fixed `` to use name instead property [#3010](https://github.com/getgrav/grav/pull/3010)
+ * Fixed behavior of opposite filters in `Pages::getCollection()` to match Grav 1.6 [#3216](https://github.com/getgrav/grav/pull/3216)
+ * Fixed modular content with missing template file ending up using non-modular template [#3218](https://github.com/getgrav/grav/issues/3218)
+ * Fixed broken attachment image in Flex Objects Admin when `destination: self@` used [#3225](https://github.com/getgrav/grav/issues/3225)
+ * Fixed bug in page content with both markdown and twig enabled [#3223](https://github.com/getgrav/grav/issues/3223)
+
+# v1.7.5
+## 02/01/2021
+
+1. [](#bugfix)
+ * Revert: Fixed page save failing because of uploaded images [#3191](https://github.com/getgrav/grav/issues/3191) - breaking save
+
+# v1.7.4
+## 02/01/2021
+
+1. [](#new)
+ * Added `FlexForm::setSubmitMethod()` to customize form submit action
+1. [](#improved)
+ * Improved GPM error handling
+1. [](#bugfix)
+ * Fixed `bin/gpm uninstall` script not working because of bad typehint [#3172](https://github.com/getgrav/grav/issues/3172)
+ * Fixed `login: visibility_requires_access` not working in pages [#3176](https://github.com/getgrav/grav/issues/3176)
+ * Fixed cannot change image format [#3173](https://github.com/getgrav/grav/issues/3173)
+ * Fixed saving page in expert mode [#3174](https://github.com/getgrav/grav/issues/3174)
+ * Fixed exception in `$flexPage->frontmatter()` method when setting value
+ * Fixed `onBlueprintCreated` event being called multiple times in `Flex Pages` [grav-plugin-flex-objects#97](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/97)
+ * Fixed wrong ordering in page collections if `intl` extension has been enabled [#3167](https://github.com/getgrav/grav/issues/3167)
+ * Fixed page redirect to the first visible child page (needs to be routable and published, too)
+ * Fixed untranslated module pages showing up in the menu
+ * Fixed page save failing because of uploaded images [#3191](https://github.com/getgrav/grav/issues/3191)
+ * Fixed incorrect config lookup for loading in `ImageLoadingTrait` [#3192](https://github.com/getgrav/grav/issues/3192)
+
+# v1.7.3
+## 01/21/2021
+
+1. [](#improved)
+ * IMPORTANT - Please [checkout the process](https://getgrav.org/blog/grav-170-cli-self-upgrade-bug) to `self-upgrade` from CLI if you are on **Grav 1.7.0 or 1.7.1**
+ * Added support for symlinking individual plugins and themes by using `bin/grav install -p myplugin` or `-t mytheme`
+ * Added support for symlinking plugins and themes with `hebe.json` file to support custom folder structures
+ * Added support for running post-install scripts in `bin/gpm selfupgrade` if Grav was updated manually
+1. [](#bugfix)
+ * Fixed default GPM Channel back to 'stable' - this was inadvertently left as 'testing' [#3163](https://github.com/getgrav/grav/issues/3163)
+ * Fixed broken stream initialization if `environment://` paths aren't streams
+ * Fixed Clockwork debugger in sub-folder multi-site setups
+ * Fixed `Unsupported option "curl" passed to "Symfony\Component\HttpClient\CurlHttpClient"` in `bin/gpm selfupgrade` [#3165](https://github.com/getgrav/grav/issues/3165)
+
+# v1.7.2
+## 01/21/2021
+
+1. [](#improved)
+ * This release was pulled due to a bug in the installer, 1.7.3 replaces it.
+
+# v1.7.1
+## 01/20/2021
+
+1. [](#bugfix)
+ * Fixed fatal error when `site.taxonomies` contains a bad value
+ * Sanitize valid Page extensions from `Page::template_format()`
+ * Fixed `bin/gpm index` erroring out [#3158](https://github.com/getgrav/grav/issues/3158)
+ * Fixed `bin/gpm selfupgrade` failing to report failed Grav update [#3116](https://github.com/getgrav/grav/issues/3116)
+ * Fixed `bin/gpm selfupgrade` error on `Call to undefined method` [#3160](https://github.com/getgrav/grav/issues/3160)
+ * Flex Pages: Fixed fatal error when trying to move a page to Root (/) [#3161](https://github.com/getgrav/grav/issues/3161)
+ * Fixed twig parsing errors in pages where twig is parsed after markdown [#3162](https://github.com/getgrav/grav/issues/3162)
+ * Fixed `lighttpd.conf` access-deny rule [#1876](https://github.com/getgrav/grav/issues/1876)
+ * Fixed page metadata being double-escaped [#3121](https://github.com/getgrav/grav/issues/3121)
+
+# v1.7.0
+## 01/19/2021
+
+1. [](#new)
+ * Requires **PHP 7.3.6**
+ * Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post
+ * Read the full list of all changes in the [Changelog on GitHub](https://github.com/getgrav/grav/blob/1.7.0/CHANGELOG.md)
+ * Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading!
+ * Added support for overriding configuration by using environment variables
+ * Use PHP 7.4 serialization (the old `Serializable` methods are now final and cannot be overridden)
+ * Enabled `ETag` setting by default for 304 responses
+ * Added `FlexCollection::getDistinctValues()` to get all the assigned values from the field
+ * `Flex Pages` method `$page->header()` returns `\Grav\Common\Page\Header` object, old `Page` class still returns `stdClass`
+1. [](#improved)
+ * Make it possible to use an absolute path when loading a blueprint
+ * Make serialize methods final in `ContentBlock`, `AbstractFile`, `FormTrait`, `ObjectCollectionTrait` and `ObjectTrait`
+ * Added support for relative paths in `PageObject::getLevelListing()` [#3110](https://github.com/getgrav/grav/issues/3110)
+ * Better `--env` and `--lang` support for `bin/grav`, `bin/gpm` and `bin/plugin` console commands
+ * **BC BREAK** Shorthand for `--env`: `-e` will not work anymore as it conflicts with some plugins
+ * Added support for locking the `start` and `limit` in a Page Collection
+1. [](#bugfix)
+ * Fixed port issue with `system.custom_base_url`
+ * Hide errors with `exif_read_data` in `ImageFile`
+ * Fixed unserialize in `MarkdownFormatter` and `Framework\File` classes
+ * Fixed pages with session messages should never be cached [#3108](https://github.com/getgrav/grav/issues/3108)
+ * Fixed `Filesystem::normalize()` with dot-dot paths
+ * Fixed Flex sorting issues [grav-plugin-flex-objects#92](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/92)
+ * Fixed Clockwork missing dumped arrays and objects
+ * Fixed fatal error in PHP 8 when trying to access root page
+ * Fixed Array->String conversion error when `languages:translations: false` [admin#1896](https://github.com/getgrav/grav-plugin-admin/issues/1896)
+ * Fixed `Inflector` methods when translation is missing `GRAV.INFLECTOR_*` translations
+ * Fixed exception when changing parent of new page [grav-plugin-admin#2018](https://github.com/getgrav/grav-plugin-admin/issues/2018)
+ * Fixed ordering issue with moving pages [grav-plugin-admin#2015](https://github.com/getgrav/grav-plugin-admin/issues/2015)
+ * Fixed Flex Pages cache not invalidating if saving an old `Page` object [#3152](https://github.com/getgrav/grav/issues/3152)
+ * Fixed multiple issues with `system.language.translations: false`
+ * Fixed page collections containing dummy items for untranslated default language [#2985](https://github.com/getgrav/grav/issues/2985)
+ * Fixed streams in `setup.php` being overridden by `system/streams.yaml` [#2450](https://github.com/getgrav/grav/issues/2450)
+ * Fixed `ERR_TOO_MANY_REDIRECTS` with HTTPS = 'On' [#3155](https://github.com/getgrav/grav/issues/3155)
+ * Fixed page collection pagination not behaving as it did in Grav 1.6
+
+# v1.7.0-rc.20
+## 12/15/2020
+
+1. [](#new)
+ * Update phpstan to version 0.12
+ * Auto-Escape enabled by default. Manually enable **Twig Compatibility** and disable **Auto-Escape** to use the old setting.
+ * Updated unit tests to use codeception 4.1
+ * Added support for setting `GRAV_ENVIRONMENT` by using environment variable or a constant
+ * Added support for setting `GRAV_SETUP_PATH` by using environment variable (constant already worked)
+ * Added support for setting `GRAV_ENVIRONMENTS_PATH` by using environment variable or a constant
+ * Added support for setting `GRAV_ENVIRONMENT_PATH` by using environment variable or a constant
+1. [](#improved)
+ * Improved `bin/grav install` command
+1. [](#bugfix)
+ * Fixed potential error when upgrading Grav
+ * Fixed broken list in `bin/gpm index` [#3092](https://github.com/getgrav/grav/issues/3092)
+ * Fixed CLI/GPM command failures returning 0 (success) value [#3017](https://github.com/getgrav/grav/issues/3017)
+ * Fixed unimplemented `PageObject::getOriginal()` call [#3098](https://github.com/getgrav/grav/issues/3098)
+ * Fixed `Argument 1 passed to Grav\Common\User\DataUser\User::filterUsername() must be of the type string` [#3101](https://github.com/getgrav/grav/issues/3101)
+ * Fixed broken check if php exif module is enabled in `ImageFile::fixOrientation()`
+ * Fixed `StaticResizeTrait::resize()` bad image height/width attributes if `null` values are passed to the method
+ * Fixed twig script/style tag `{% script 'file.js' at 'bottom' %}`, replaces broken `in` operator [#3084](https://github.com/getgrav/grav/issues/3084)
+ * Fixed dropped query params when `?` is preceded with `/` [#2964](https://github.com/getgrav/grav/issues/2964)
+
+# v1.7.0-rc.19
+## 12/02/2020
+
+1. [](#bugfix)
+ * Updated composer libraries with latest Toolbox v1.5.6 that contains critical fixes
+
+# v1.7.0-rc.18
+## 12/02/2020
+
+1. [](#new)
+ * Set minimum requirements to **PHP 7.3.6**
+ * Updated Clockwork to v5.0
+ * Added `FlexDirectoryInterface` interface
+ * Renamed `PageCollectionInterface::nonModular()` into `PageCollectionInterface::pages()` and deprecated the old method
+ * Renamed `PageCollectionInterface::modular()` into `PageCollectionInterface::modules()` and deprecated the old method'
+ * Upgraded `bin/composer.phar` to `2.0.2` which is all new and much faster
+ * Added search option `same_as` to Flex Objects
+ * Added PHP 8 compatible `function_exists()`: `Utils::functionExists()`
+ * New sites have `compatibility` features turned off by default, upgrading from older versions will keep the settings on
+1. [](#improved)
+ * Updated bundled JQuery to latest version `3.5.1`
+ * Forward a `sid` to GPM when downloading a premium package via CLI
+ * Allow `JsonFormatter` options to be passed as a string
+ * Hide Flex Pages frontend configuration (not ready for production use)
+ * Improve Flex configuration: gather views together in blueprint
+ * Added XSS detection to all forms. See [documentation](https://learn.getgrav.org/17/forms/forms/form-options#xss-checks)
+ * Better handling of missing repository index [grav-plugin-admin#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916)
+ * Added support for having all sites / environments under `user/env` folder [#3072](https://github.com/getgrav/grav/issues/3072)
+ * Added `FlexObject::refresh()` method to make sure object is up to date
+1. [](#bugfix)
+ * *Menu Visibility Requires Access* Security option setting wrong frontmatter [login#265](https://github.com/getgrav/grav-plugin-login/issues/265)
+ * Accessing page with unsupported file extension (jpg, pdf, xsl) will use wrong mime type [#3031](https://github.com/getgrav/grav/issues/3031)
+ * Fixed media crashing on a bad image
+ * Fixed bug in collections where filter `type: false` did not work
+ * Fixed `print_r()` in twig
+ * Fixed sorting by groups in `Flex Users`
+ * Changing `Flex Page` template causes the other language versions of that page to lose their content [admin#1958](https://github.com/getgrav/grav-plugin-admin/issues/1958)
+ * Fixed plugins getting initialized multiple times (by CLI commands for example)
+ * Fixed `header.admin.children_display_order` in Flex Pages to work just like with regular pages
+ * Fixed `Utils::isFunctionDisabled()` method if there are spaces in `disable_functions` [#3023](https://github.com/getgrav/grav/issues/3023)
+ * Fixed potential fatal error when creating flex index using cache [#3062](https://github.com/getgrav/grav/issues/3062)
+ * Fixed fatal error in `CompiledFile` if the cached version is broken
+ * Fixed updated media missing from media when editing Flex Object after page reload
+ * Fixed issue with `config-default@` breaking on set [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1971)
+ * Escape titles in Flex pages list [flex-objects#84](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/84)
+ * Fixed Purge successful message only working in Scheduler but broken in CLI and Admin [#1935](https://github.com/getgrav/grav-plugin-admin/issues/1935)
+ * Fixed `system://` stream is causing issues in Admin, making Media tab to disappear and possibly causing other issues [#3072](https://github.com/getgrav/grav/issues/3072)
+ * Fixed CLI self-upgrade from Grav 1.6 [#3079](https://github.com/getgrav/grav/issues/3079)
+ * Fixed `bin/grav yamllinter -a` and `-f` not following symlinks [#3080](https://github.com/getgrav/grav/issues/3080)
+ * Fixed `|safe_email` filter to return safe and escaped UTF-8 HTML [#3072](https://github.com/getgrav/grav/issues/3072)
+ * Fixed exception in CLI GPM and backup commands when `php-zip` is not enabled [#3075](https://github.com/getgrav/grav/issues/3075)
+ * Fix for XSS advisory [GHSA-cvmr-6428-87w9](https://github.com/getgrav/grav/security/advisories/GHSA-cvmr-6428-87w9)
+ * Fixed Flex and Page ordering to be natural and case insensitive [flex-objects#87](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/87)
+ * Fixed plugin/theme priority ordering to be numeric
+
+# v1.7.0-rc.17
+## 10/07/2020
+
+1. [](#new)
+ * Added a `Uri::getAllHeaders()` compatibility function
+1. [](#improved)
+ * Fall back through various templates scenarios if they don't exist in theme to avoid unhelpful error.
+ * Added default templates for `external.html.twig`, `default.html.twig`, and `modular.html.twig`
+ * Improve Media classes
+ * _POTENTIAL BREAKING CHANGE:_ Added reload argument to `FlexStorageInterface::getMetaData()`
+1. [](#bugfix)
+ * Fixed `Security::sanitizeSVG()` creating an empty file if SVG file cannot be parsed
+ * Fixed infinite loop in blueprints with `extend@` to a parent stream
+ * Added missing `Stream::create()` method
+ * Added missing `onBlueprintCreated` event for Flex Pages
+ * Fixed `onBlueprintCreated` firing multiple times recursively
+ * Fixed media upload failing with custom folders
+ * Fixed `unset()` in `ObjectProperty` class
+ * Fixed `FlexObject::freeMedia()` method causing media to become null
+ * Fixed bug in `Flex Form` making it impossible to set nested values
+ * Fixed `Flex User` avatar when using folder storage, also allow multiple images
+ * Fixed Referer reference during GPM calls.
+ * Fixed fatal error with toggled lists
+
+# v1.7.0-rc.16
+## 09/01/2020
+
+1. [](#new)
+ * Added a new `svg_image()` twig function to make it easier to 'include' SVG source in Twig
+ * Added a helper `Utils::fullPath()` to get the full path to a file be it stream, relative, etc.
+1. [](#improved)
+ * Added `themes` to cached blueprints and configuration
+1. [](#bugfix)
+ * Fixed `Flex Pages` issue with `getRoute()` returning path with language prefix for default language if set not to do that
+ * Fixed `Flex Pages` bug where reordering pages causes page content to disappear if default language uses wrong extension (`.md` vs `.en.md`)
+ * Fixed `Flex Pages` bug where `onAdminSave` passes page as `$event['page']` instead of `$event['object']` [#2995](https://github.com/getgrav/grav/issues/2995)
+ * Fixed `Flex Pages` bug where changing a modular page template added duplicate file [admin#1899](https://github.com/getgrav/grav-plugin-admin/issues/1899)
+ * Fixed `Flex Pages` bug where renaming slug causes bad ordering range after save [#2997](https://github.com/getgrav/grav/issues/2997)
+
+# v1.7.0-rc.15
+## 07/22/2020
+
+1. [](#bugfix)
+ * Fixed Flex index file caching [#2962](https://github.com/getgrav/grav/issues/2962)
+ * Fixed various issues with Exif data reading and images being incorrectly rotated [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923)
+
+# v1.7.0-rc.14
+## 07/09/2020
+
+1. [](#improved)
+ * Added ability to `noprocess` specific items only in Link/Image Excerpts, e.g. `http://foo.com/page?id=foo&target=_blank&noprocess=id` [#2954](https://github.com/getgrav/grav/pull/2954)
+1. [](#bugfix)
+ * Regression: Default language fix broke `Language::getLanguageURLPrefix()` and `Language::isIncludeDefaultLanguage()` methods when not using multi-language
+ * Reverted `Language::getDefault()` and `Language::getLanguage()` to return false again because of plugin compatibility (updated docblocks)
+ * Fixed UTF-8 issue in `Excerpts::getExcerptsFromHtml`
+ * Fixed some compatibility issues with recent Changes to `Assets` handling
+ * Fixed issue with `CSS_IMPORTS_REGEX` breaking with complex URLs [#2958](https://github.com/getgrav/grav/issues/2958)
+ * Moved duplicated `CSS_IMPORT_REGEX` to local variable in `AssetUtilsTrait::moveImports()`
+ * Fixed page media only accepting images [#2943](https://github.com/getgrav/grav/issues/2943)
+
+# v1.7.0-rc.13
+## 07/01/2020
+
+1. [](#new)
+ * Added support for uploading and deleting images directly in `Media`
+ * Added new `onAfterCacheClear` event
+1. [](#improved)
+ * Improved `CvsFormatter` to attempt to encode non-scalar variables into JSON before giving up
+ * Moved image loading into its own trait to be used by images+static images
+ * Adjusted asset types to enable extension of assets in class [#2937](https://github.com/getgrav/grav/pull/2937)
+ * Composer update for vendor library updates
+ * Updated bundled `composer.phar` to `2.0.0-dev`
+1. [](#bugfix)
+ * Fixed `MediaUploadTrait::copyUploadedFile()` not adding uploaded media to the collection
+ * Fixed regression in saving media to a new Flex Object [admin#1867](https://github.com/getgrav/grav-plugin-admin/issues/1867)
+ * Fixed `Trying to get property 'username' of non-object` error in Flex [flex-objects#62](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/62)
+ * Fixed retina images not working in Flex [flex-objects#64](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/64)
+ * Fixed plugin initialization in CLI
+ * Fixed broken logic in `Page::topParent()` when dealing with first-level pages
+ * Fixed broken `Flex Page` authorization for groups
+ * Fixed missing `onAdminSave` and `onAdminAfterSave` events when using `Flex Pages` and `Flex Users` [flex-objects#58](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/58)
+ * Fixed new `User Group` allowing bad group name to be saved [admin#1917](https://github.com/getgrav/grav-plugin-admin/issues/1917)
+ * Fixed `Language::getDefault()` returning false and not 'en'
+ * Fixed non-text links in `Excerpts::getExcerptFromHtml`
+ * Fixed CLI commands not properly intializing Plugins so events can fire
+
+# v1.7.0-rc.12
+## 06/08/2020
+
+1. [](#improved)
+ * Changed `Folder::hasChildren` to `Folder::countChildren`
+ * Added `Content Editor` option to user account blueprint
+1. [](#bugfix)
+ * Fixed new `Flex Page` not having correct form fields for the page type
+ * Fixed new `Flex User` erroring out on save (thanks @mikebi42)
+ * Fixed `Flex Object` request cache clear when saving object
+ * Fixed blueprint value filtering in lists [#2923](https://github.com/getgrav/grav/issues/2923)
+ * Fixed blueprint for `system.pages.hide_empty_folders` [#1925](https://github.com/getgrav/grav/issues/2925)
+ * Fixed file field in `Flex Objects` (use `Grav\Common\Flex\Types\GenericObject` instead of `FlexObject`) [flex-objects#37](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/37)
+ * Fixed saving nested file fields in `Flex Objects` [flex-objects#34](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/34)
+ * JSON Route of homepage with no ‘route’ set is valid [form#425](https://github.com/getgrav/grav-plugin-form/issues/425)
+
+# v1.7.0-rc.11
+## 05/14/2020
+
+1. [](#new)
+ * Added support for native `loading=lazy` attributes on images. Can be set in `system.images.defaults` or per md image with `?loading=lazy` [#2910](https://github.com/getgrav/grav/issues/2910)
+1. [](#improved)
+ * Added `PageCollection::all()` to mimic Pages class
+ * Added system configuration support for `HTTP_X_Forwarded` headers (host disabled by default)
+ * Updated `PHPUserAgentParser` to 1.0.0
+ * Improved docblocks
+ * Fixed some phpstan issues
+ * Tighten vendor requirements
+1. [](#bugfix)
+ * Fix for uppercase image extensions
+ * Fix for `&` errors in HTML when passed to `Excerpts.php`
+
+# v1.7.0-rc.10
+## 04/30/2020
+
+1. [](#new)
+ * Changed `Response::get()` used by **GPM/Admin** to use [Symfony HttpClient v4.4](https://symfony.com/doc/current/components/http_client.html) (`composer install --nodev` required for Git installations)
+ * Added new `Excerpts::processLinkHtml()` method
+1. [](#bugfix)
+ * Fixed `Flex Pages` admin with PHP `intl` extension enabled when using custom page order
+ * Fixed saving non-numeric-prefix `Flex Page` changing to numeric-prefix [flex-objects#56](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/56)
+ * Copying `Flex Page` in admin does nothing [flex-objects#55](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/55)
+ * Force GPM progress to be between 0-100%
+
+# v1.7.0-rc.9
+## 04/27/2020
+
+1. [](#new)
+ * Support for `webp` image format in Page Media [#1168](https://github.com/getgrav/grav/issues/1168)
+ * Added `Route::getBase()` method
+1. [](#improved)
+ * Support symlinks when saving `File`
+1. [](#bugfix)
+ * Fixed flex objects with integer keys not working [#2863](https://github.com/getgrav/grav/issues/2863)
+ * Fixed `Pages::instances()` returning null values when using `Flex Pages` [#2889](https://github.com/getgrav/grav/issues/2889)
+ * Fixed Flex Page parent `header.admin.children_display_order` setting being ignored in Admin [#2881](https://github.com/getgrav/grav/issues/2881)
+ * Implemented missing Flex `$pageCollection->batch()` and `$pageCollection->order()` methods
+ * Fixed user avatar creation for new `Flex Users` when using folder storage
+ * Fixed `Trying to access array offset on value of type null` PHP 7.4 error in `Plugin.php`
+ * Fixed Gregwar Image library using `.jpeg` for cached images, rather use `.jpg`
+ * Fixed `Flex Pages` with `00.home` page not having ordering set
+ * Fixed `Flex Pages` not updating empty content on save [#2890](https://github.com/getgrav/grav/issues/2890)
+ * Fixed creating new Flex User with file storage
+ * Fixed saving new `Flex Object` with custom key
+ * Fixed broken `Plugin::config()` method
+
+# v1.7.0-rc.8
+## 03/19/2020
+
+1. [](#new)
+ * Added `MediaTrait::freeMedia()` method to free media (and memory)
+ * Added `Folder::hasChildren()` method to determine if a folder has child folders
+1. [](#improved)
+ * Save memory when updating large flex indexes
+ * Better `Content-Encoding` handling in Apache when content compression is disabled [#2619](https://github.com/getgrav/grav/issues/2619)
+1. [](#bugfix)
+ * Fixed creating new `Flex User` when folder storage has been selected
+ * Fixed some bugs in Flex root page methods
+ * Fixed bad default redirect code in `ControllerResponseTrait::createRedirectResponse()`
+ * Fixed issue with PHP `HTTP_X_HTTP_METHOD_OVERRIDE` [#2847](https://github.com/getgrav/grav/issues/2847)
+ * Fixed numeric usernames not working in `Flex Users`
+ * Implemented missing Flex `$page->move()` method
+
+# v1.7.0-rc.7
+## 03/05/2020
+
+1. [](#new)
+ * Added `Session::regenerateId()` method to properly prevent session fixation issues
+ * Added configuration option `system.strict_mode.blueprint_compat` to maintain old `validation: strict` behavior [#1273](https://github.com/getgrav/grav/issues/1273)
+1. [](#improved)
+ * Improved Flex events
+ * Updated CLI commands to use the new methods to initialize Grav
+1. [](#bugfix)
+ * Fixed Flex Pages having broken `isFirst()`, `isLast()`, `prevSibling()`, `nextSibling()` and `adjacentSibling()`
+ * Fixed broken ordering sometimes when saving/moving visible `Flex Page` [#2837](https://github.com/getgrav/grav/issues/2837)
+ * Fixed ordering being lost when saving modular `Flex Page`
+ * Fixed `validation: strict` not working in blueprints (see `system.strict_mode.blueprint_compat` setting) [#1273](https://github.com/getgrav/grav/issues/1273)
+ * Fixed `Blueprint::extend()` and `Blueprint::embed()` not initializing dynamic properties
+ * Fixed fatal error on storing flex flash using new object without a key
+ * Regression: Fixed unchecking toggleable having no effect in Flex forms
+ * Fixed changing page template in Flex Pages [#2828](https://github.com/getgrav/grav/issues/2828)
+
+# v1.7.0-rc.6
+## 02/11/2020
+
+1. [](#new)
+ * Plugins & Themes: Call `$plugin->autoload()` and `$theme->autoload()` automatically when object gets initialized
+ * CLI: Added `$grav->initializeCli()` method
+ * Flex Directory: Implemented customizable configuration
+ * Flex Storages: Added support for renaming directory entries
+1. [](#improved)
+ * Vendor updates to latest
+1. [](#bugfix)
+ * Regression: Fixed fatal error in blueprints [#2811](https://github.com/getgrav/grav/issues/2811)
+ * Regression: Fixed bad method call in FlexDirectory::getAuthorizeRule()
+ * Regression: Fixed fatal error in admin if the site has custom permissions in `onAdminRegisterPermissions`
+ * Regression: Fixed flex user index with folder storage
+ * Regression: Fixed fatal error in `bin/plugin` command
+ * Fixed `FlexObject::triggerEvent()` not emitting events [#2816](https://github.com/getgrav/grav/issues/2816)
+ * Grav 1.7: Fixed saving Flex configuration with ignored values becoming null
+ * Grav 1.7: Fixed `bin/plugin` initialization
+ * Grav 1.7: Fixed Flex Page cache key not taking account active language
+
+# v1.7.0-rc.5
+## 02/03/2020
+
+1. [](#bugfix)
+ * Regression: Flex not working in PHP 7.2 or older
+ * Fixed creating first user from admin not clearing Flex User directory cache [#2809](https://github.com/getgrav/grav/issues/2809)
+ * Fixed Flex Pages allowing root page to be deleted
+
+# v1.7.0-rc.4
+## 02/03/2020
+
+1. [](#new)
+ * _POTENTIAL BREAKING CHANGE:_ Upgraded Parsedown to 1.7 for Parsedown-Extra 0.8. Plugins that extend Parsedown may need a fix to render as HTML
+ * Added `$grav['flex']` to access all registered Flex Directories
+ * Added `$grav->dispatchEvent()` method for PSR-14 events
+ * Added `FlexRegisterEvent` which triggers when `$grav['flex']` is being accessed the first time
+ * Added Flex cache configuration options
+ * Added `PluginsLoadedEvent` which triggers after plugins have been loaded but not yet initialized
+ * Added `SessionStartEvent` which triggers when session is started
+ * Added `PermissionsRegisterEvent` which triggers when `$grav['permissions']` is being accessed the first time
+ * Added support for Flex Directory specific configuration
+ * Added support for more advanced ACL
+ * Added `flatten_array` filter to form field validation
+ * Added support for `security@: or: [admin.super, admin.pages]` in blueprints (nested AND/OR mode support)
+1. [](#improved)
+ * Blueprint validation: Added `validate: value_type: bool|int|float|string|trim` to `array` to filter all the values inside the array
+ * Twig `url()` takes now third parameter (`true`) to return URL on non-existing file instead of returning false
+1. [](#bugfix)
+ * Grav 1.7: Fixed blueprint loading issues [#2782](https://github.com/getgrav/grav/issues/2782)
+ * Fixed PHP 7.4 compatibility issue with `Stream`
+ * Fixed new `Flex Users` being stored with wrong filename, login issues [#2785](https://github.com/getgrav/grav/issues/2785)
+ * Fixed `ignore_empty: true` not removing empty values in blueprint filtering
+ * Fixed `{{ false|string }}` twig to return '0' instead of ''
+ * Fixed twig `url()` failing if stream has extra slash in it (e.g. `user:///data`)
+ * Fixed `Blueprint::filter()` returning null instead of array if there is nothing to return
+ * Fixed `Cannot use a scalar value as an array` error in `Utils::arrayUnflattenDotNotation()`, ignore nested structure instead
+ * Fixed `Route` instance in multi-site setups
+ * Fixed `system.translations: false` breaking `Inflector` methods
+ * Fixed filtering ignored (eg. `security@: admin.super`) fields causing `Flex Objects` to lose data on save
+ * Grav 1.7: Fixed `Flex Pages` unserialize issues if Flex-Objects Plugin has not been installed
+ * Grav 1.7: Require Flex-Objects Plugin to edit Flex Accounts
+ * Grav 1.7: Fixed bad result on testing `isPage()` when using Flex Pages
+
+# v1.7.0-rc.3
+## 01/02/2020
+
+1. [](#new)
+ * Added root page support for `Flex Pages`
+1. [](#improved)
+ * Twig filter `|yaml_serialize`: added support for `JsonSerializable` objects and other array-like objects
+ * Added support for returning Flex Page specific permissions for admin and testing
+ * Updated copyright dates to `2020`
+ * Various vendor updates
+1. [](#bugfix)
+ * Grav 1.7: Fixed error on page initialization [#2753](https://github.com/getgrav/grav/issues/2753)
+ * Fixed checking ACL for another user (who is not currently logged in) in a Flex Object or Directory
+ * Fixed bug in Windows where `Filesystem::dirname()` returns backslashes
+ * Fixed Flex object issues in Windows [#2773](https://github.com/getgrav/grav/issues/2773)
+
+# v1.7.0-rc.2
+## 12/04/2019
+
+1. [](#new)
+ * Updated Symfony Components to 4.4
+ * Added support for page specific CRUD permissions (`Flex Pages` only)
+ * Added new `-r ` option for Scheduler CLI command to force-run a job [#2720](https://github.com/getgrav/grav/issues/2720)
+ * Added `Utils::isAssoc()` and `Utils::isNegative()` helper methods
+ * Changed `UserInterface::authorize()` to return `null` having the same meaning as `false` if access is denied because of no matching rule
+ * Changed `FlexAuthorizeInterface::isAuthorized()` to return `null` having the same meaning as `false` if access is denied because of no matching rule
+ * Moved all Flex type classes under `Grav\Common\Flex`
+ * DEPRECATED `Grav\Common\User\Group` in favor of `$grav['user_groups']`, which contains Flex UserGroup collection
+ * DEPRECATED `$page->modular()` in favor of `$page->isModule()` for better readability
+ * Fixed phpstan issues in all code up to level 3
+1. [](#improved)
+ * Improved twig `|array` filter to work with iterators and objects with `toArray()` method
+ * Updated Flex `SimpleStorage` code to feature match the other storages
+ * Improved user and group ACL to support deny permissions (`Flex Users` only)
+ * Improved twig `authorize()` function to work better with nested rule parameters
+ * Output the current username that Scheduler is using if crontab not setup
+ * Translations: rename MODULAR to MODULE everywhere
+ * Optimized `Flex Pages` collection filtering
+ * Frontend optimizations for `Flex Pages`
+1. [](#bugfix)
+ * Regression: Fixed Grav update bug [#2722](https://github.com/getgrav/grav/issues/2722)
+ * Fixed fatal error when calling `{{ grav.undefined }}`
+ * Grav 1.7: Reverted `$object->getStorageKey()` interface as it was not a good idea, added `getMasterKey()` for pages
+ * Grav 1.7: Fixed logged in user being able to delete his own account from admin account manager
+
+# v1.7.0-rc.1
+## 11/06/2019
+
+1. [](#new)
+ * Added `Flex Pages` to Grav core and removed Flex Objects plugin dependency
+ * Added `Utils::simpleTemplate()` method for very simple variable templating
+ * Added `array_diff()` twig function
+ * Added `template_from_string()` twig function
+ * Updated Symfony Components to 4.3
+1. [](#improved)
+ * Improved `Scheduler` cron command check and more useful CLI information
+ * Improved `Flex Users`: obey blueprints and allow Flex to be used in admin only
+ * Improved `Flex` to support custom site template paths
+ * Changed Twig `{% cache %}` tag to not need unique key, and `lifetime` is now optional
+ * Added mime support for file formatters
+ * Updated built-in `composer.phar` to latest `1.9.0`
+ * Updated vendor libraries
+ * Use `Symfony EventDispatcher` directly and not rockettheme/toolbox wrapper
+1. [](#bugfix)
+ * Fixed exception caused by missing template type based on `Accept:` header [#2705](https://github.com/getgrav/grav/issues/2705)
+ * Fixed `Page::untranslatedLanguages()` not being symmetrical to `Page::translatedLanguages()`
+ * Fixed `Flex Pages` not calling `onPageProcessed` event when cached
+ * Fixed phpstan issues in Framework up to level 7
+ * Fixed issue with duplicate configuration settings in Flex Directory
+ * Fixed fatal error if there are numeric folders in `Flex Pages`
+ * Fixed error on missing `Flex` templates in if `Flex Objects` plugin isn't installed
+ * Fixed `PageTranslateTrait::getAllLanguages()` and `getAllLanguages()` to include default language
+ * Fixed multi-language saving issues with default language in `Flex Pages`
+ * Selfupgrade CLI: Fixed broken selfupgrade assets reference [#2681](https://github.com/getgrav/grav/issues/2681)
+ * Grav 1.7: Fixed PHP 7.1 compatibility issues
+ * Grav 1.7: Fixed fatal error in multi-site setups
+ * Grav 1.7: Fixed `Flex Pages` routing if using translated slugs or `system.hide_in_urls` setting
+ * Grav 1.7: Fixed bug where Flex index file couldn't be disabled
+
+# v1.7.0-beta.10
+## 10/03/2019
+
+1. [](#improved)
+ * Flex: Removed extra exists check when creating object (messes up "non-existing" pages)
+ * Support customizable null character replacement in `CSVFormatter::decode()`
+1. [](#bugfix)
+ * Fixed wrong Grav param separator when using `Route` class
+ * Fixed Flex User Avatar not fully backwards compatible with old user
+ * Grav 1.7: Fixed prev/next page missing pages if pagination was turned on in page header
+ * Grav 1.7: Reverted setting language for every page during initialization
+ * Grav 1.7: Fixed numeric language inconsistencies
+
+# v1.7.0-beta.9
+## 09/26/2019
+
+1. [](#new)
+ * Added a new `{% cache %}` Twig tag eliminating need for `twigcache` extension.
+1. [](#improved)
+ * Improved blueprint initialization in Flex Objects (fixes content aware fields)
+ * Improved Flex FolderStorage class to better hide storage specific logic
+ * Exception will output a badly formatted line in `CsvFormatter::decode()`
+1. [](#bugfix)
+ * Fixed error when activating Flex Accounts in GRAV system configuration (PHP 7.1)
+ * Fixed Grav parameter handling in `RouteFactory::createFromString()`
+
+# v1.7.0-beta.8
+## 09/19/2019
+
+1. [](#new)
+ * Added new `Security::sanitizeSVG()` function
+ * Backwards compatibility break: `FlexStorageInterface::getStoragePath()` and `getMediaPath()` can now return null
+1. [](#improved)
+ * Several FlexObject loading improvements
+ * Added `bin/grav page-system-validator [-r|--record] [-c|--check]` to test Flex Pages
+ * Improved language support for `Route` class
+1. [](#bugfix)
+ * Regression: Fixed language fallback
+ * Regression: Fixed translations when language code is used for non-language purposes
+ * Regression: Allow SVG avatar images for users
+ * Fixed error in `Session::getFlashObject()` if Flex Form is being used
+ * Fixed broken Twig `dump()`
+ * Fixed `Page::modular()` and `Page::modularTwig()` returning `null` for folders and other non-initialized pages
+ * Fixed 404 error when you click to non-routable menu item with children: redirect to the first child instead
+ * Fixed wrong `Pages::dispatch()` calls (with redirect) when we really meant to call `Pages::find()`
+ * Fixed avatars not being displayed with flex users [#2431](https://github.com/getgrav/grav/issues/2431)
+ * Fixed initial Flex Object state when creating a new objects in a form
+
+# v1.7.0-beta.7
+## 08/30/2019
+
+1. [](#improved)
+ * Improved language support
+1. [](#bugfix)
+ * `FlexForm`: Fixed some compatibility issues with Form plugin
+
+# v1.7.0-beta.6
+## 08/29/2019
+
+1. [](#new)
+ * Added experimental support for `Flex Pages` (**Flex Objects** plugin required)
+1. [](#improved)
+ * Improved `bin/grav yamllinter` CLI command by adding an option to find YAML Linting issues from the whole site or custom folder
+ * Added support for not instantiating pages, useful to speed up tasks
+ * Greatly improved speed of loading Flex collections
+1. [](#bugfix)
+ * Fixed `$page->summary()` always striping HTML tags if the summary was set by `$page->setSummary()`
+ * Fixed `Flex->getObject()` when using Flex Key
+ * Grav 1.7: Fixed enabling PHP Debug Bar causes fatal error in Gantry [#2634](https://github.com/getgrav/grav/issues/2634)
+ * Grav 1.7: Fixed broken taxonomies [#2633](https://github.com/getgrav/grav/issues/2633)
+ * Grav 1.7: Fixed unpublished blog posts being displayed on the front-end [#2650](https://github.com/getgrav/grav/issues/2650)
+
+# v1.7.0-beta.5
+## 08/11/2019
+
+1. [](#new)
+ * Added a new `bin/grav server` CLI command to easily run Symfony or PHP built-in webservers
+ * Added `hasFlexFeature()` method to test if `FlexObject` or `FlexCollection` implements a given feature
+ * Added `getFlexFeatures()` method to return all features that `FlexObject` or `FlexCollection` implements
+ * DEPRECATED `FlexDirectory::update()` and `FlexDirectory::remove()`
+ * Added `FlexStorage::getMetaData()` to get updated object meta information for listed keys
+ * Added `Language::getPageExtensions()` to get full list of supported page language extensions
+ * Added `$grav->close()` method to properly terminate the request with a response
+ * Added `Pages::getCollection()` method
+1. [](#improved)
+ * Better support for Symfony local server `symfony server:start`
+ * Make `Route` objects immutable
+ * `FlexDirectory::getObject()` can now be called without any parameters to create a new object
+ * Flex objects no longer return temporary key if they do not have one; empty key is returned instead
+ * Updated vendor libraries
+ * Moved `collection()` and `evaluate()` logic from `Page` class into `Pages` class
+1. [](#bugfix)
+ * Fixed `Form` not to use deleted flash object until the end of the request fixing issues with reset
+ * Fixed `FlexForm` to allow multiple form instances with non-existing objects
+ * Fixed `FlexObject` search by using `key`
+ * Grav 1.7: Fixed clockwork messages with arrays and objects
+
+# v1.7.0-beta.4
+## 07/01/2019
+
+1. [](#new)
+ * Updated with Grav 1.6.12 features, improvements & fixes
+ * Added new configuration option `system.debugger.censored` to hide potentially sensitive information
+ * Added new configuration option `system.languages.include_default_lang_file_extension` to keep default language in `.md` files if set to `false`
+ * Added configuration option to set fallback content languages individually for every language
+1. [](#improved)
+ * Updated Vendor libraries
+1. [](#bugfix)
+ * Fixed `.md` page to be assigned to the default language and to be listed in translated/untranslated page list
+ * Fixed `Language::getFallbackPageExtensions()` to fall back only to default language instead of going through all languages
+ * Fixed `Language::getFallbackPageExtensions()` returning wrong file extensions when passing custom page extension
+
+# v1.7.0-beta.3
+## 06/24/2019
+
+1. [](#bugfix)
+ * Fixed Clockwork on Windows machines
+ * Fixed parent field issues on Windows machines
+ * Fixed unreliable Clockwork calls in sub-folders
+
+# v1.7.0-beta.2
+## 06/21/2019
+
+1. [](#new)
+ * Updated with Grav 1.6.11 fixes
+1. [](#improved)
+ * Updated the Clockwork text
+
+# v1.7.0-beta.1
+## 06/14/2019
+
+1. [](#new)
+ * Added support for [Clockwork](https://underground.works/clockwork) developer tools (now default debugger)
+ * Added support for [Tideways XHProf](https://github.com/tideways/php-xhprof-extension) PHP Extension for profiling method calls
+ * Added Twig profiling for Clockwork debugger
+ * Added support for Twig 2.11 (compatible with Twig 1.40+)
+ * Optimization: Initialize debugbar only after the configuration has been loaded
+ * Optimization: Combine some early Grav processors into a single one
+
+# v1.6.31
+## 12/14/2020
+
+1. [](#improved)
+ * Allow all CSS and JS via `robots.txt` [#2006](https://github.com/getgrav/grav/issues/2006) [#3067](https://github.com/getgrav/grav/issues/3067)
+1. [](#bugfix)
+ * Fixed `pages` field escaping issues, needs admin update, too [admin#1990](https://github.com/getgrav/grav-plugin-admin/issues/1990)
+ * Fix `svg-image` issue with classes applied to all elements [#3068](https://github.com/getgrav/grav/issues/3068)
+
+# v1.6.30
+## 12/03/2020
+
+1. [](#bugfix)
+ * Rollback `samesite` cookie logic as it causes issues with PHP < 7.3 [#309](https://github.com/getgrav/grav/issues/3089)
+ * Fixed issue with `.travis.yml` due to GitHub API deprecated functionality
+
+# v1.6.29
+## 12/02/2020
+
+1. [](#new)
+ * Added basic support for `user/config/versions.yaml`
+1. [](#improved)
+ * Updated bundled JQuery to latest version `3.5.1`
+ * Forward a `sid` to GPM when downloading a premium package via CLI
+ * Better handling of missing repository index [grav-plugin-admin#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916)
+ * Set `grav_cli` as referrer when using `Response` from CLI
+ * Add option for timeout in `self-upgrade` command [#3013](https://github.com/getgrav/grav/pull/3013)
+ * Allow to set SameSite from system.yaml [#3063](https://github.com/getgrav/grav/pull/3063)
+ * Update media.yaml with some MS Office mimetypes [#3070](https://github.com/getgrav/grav/pull/3070)
+1. [](#bugfix)
+ * Fixed hardcoded system folder in blueprints, config and language streams
+ * Added `.htaccess` rule to block attempts to use Twig in the request URL
+ * Fix compatibility with Symfony 4.2 and up. [#3048](https://github.com/getgrav/grav/pull/3048)
+ * Fix failing example custom shceduled job. [#3050](https://github.com/getgrav/grav/pull/3050)
+ * Fix for XSS advisory [GHSA-cvmr-6428-87w9](https://github.com/getgrav/grav/security/advisories/GHSA-cvmr-6428-87w9)
+ * Fix uploads_dangerous_extensions checking [#3060](https://github.com/getgrav/grav/pull/3060)
+ * Remove redundant prefixing of `.` to extension [#3060](https://github.com/getgrav/grav/pull/3060)
+ * Check exact extension in checkFilename utility [#3061](https://github.com/getgrav/grav/pull/3061)
+
+# 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
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 42ef22d3..99ab478b 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,46 +1,133 @@
+
# Contributor Covenant Code of Conduct
## Our Pledge
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
## Our Standards
-Examples of behavior that contributes to creating a positive environment include:
+Examples of behavior that contributes to a positive environment for our
+community include:
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
-Examples of unacceptable behavior by participants include:
+Examples of unacceptable behavior include:
-* The use of sexualized language or imagery and unwelcome sexual attention or advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing others' private information, such as a physical or electronic address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a professional setting
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
-## Our Responsibilities
+## Enforcement Responsibilities
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
## Scope
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
## Enforcement
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@getgrav.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
+at [https://www.contributor-covenant.org/translations][translations].
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/LICENSE.txt b/LICENSE.txt
index cb8634f9..771734e1 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2018 Grav
+Copyright (c) 2021 Grav
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index bf3a4820..31a4a7e4 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
#  Grav
[](https://github.com/phpstan/phpstan)
-[](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
[](https://chat.getgrav.org)
- [](https://travis-ci.org/getgrav/grav) [](#backers) [](#sponsors)
+ [](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [](#backers) [](#sponsors)
Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform. There is **Zero** installation required. Just extract the ZIP archive, and you are already up and running. It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself.
@@ -21,9 +20,13 @@ The underlying architecture of Grav is designed to use well-established and _bes
# Requirements
-- PHP 7.1.3 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
+- PHP 7.3.6 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
- Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
+# Documentation
+
+The full documentation can be found from [learn.getgrav.org](https://learn.getgrav.org).
+
# QuickStart
These are the options to get Grav:
@@ -84,6 +87,11 @@ To update plugins and themes:
$ bin/gpm update
```
+## Upgrading from older version
+
+* [Upgrading to Grav 1.7](https://learn.getgrav.org/16/advanced/grav-development/grav-17-upgrade-guide)
+* [Upgrading to Grav 1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-16-upgrade-guide)
+* [Upgrading from Grav <1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-15-upgrade-guide)
# Contributing
We appreciate any contribution to Grav, whether it is related to bugs, grammar, or simply a suggestion or improvement! Please refer to the [Contributing guide](CONTRIBUTING.md) for more guidance on this topic.
@@ -128,7 +136,14 @@ See [LICENSE](LICENSE.txt)
# Running Tests
-First install the dev dependencies by running `composer update` from the Grav root.
+First install the dev dependencies by running `composer install` from the Grav root.
+
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
Windows users should use the `composer test-windows` command.
You can also run a single unit test file, e.g. `composer test tests/unit/Grav/Common/AssetsTest.php`
+
+To run phpstan tests, you should run:
+
+* `composer phpstan` for global tests
+* `composer phpstan-framework` for more strict tests
+* `composer phpstan-plugins` to test all installed plugins
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..30830c78
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+We are focusing our security updates on the following versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 1.7.x | :white_check_mark: |
+| 1.6.x | :warning: |
+| < 1.6 | :x: |
+
+## :warning: Versions
+
+Versions with :warning: will be supported for security issues, however you won't be able to update to them, you will need to manually update through the [`direct-install` command](https://learn.getgrav.org/17/admin-panel/tools).
+
+If you cannot update to the latest stable version available because, for example, your server does not meet the minimum PHP requirements, you can manually install a previous version by downloading the package from our Releases directory (https://github.com/getgrav/grav/releases).
+
+## Reporting a Vulnerability
+
+Please contact security@getgrav.org with a detailed explaination of the security issue found and we will work with you to get it resolved as fast as possible.
diff --git a/assets/.gitkeep b/assets/.gitkeep
index e69de29b..33a9aed7 100644
--- a/assets/.gitkeep
+++ b/assets/.gitkeep
@@ -0,0 +1 @@
+/* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved. */
diff --git a/backup/default_site_backup--20211004172648.zip b/backup/default_site_backup--20211004172648.zip
new file mode 100644
index 00000000..4e1d42b4
Binary files /dev/null and b/backup/default_site_backup--20211004172648.zip differ
diff --git a/bin/composer.phar b/bin/composer.phar
index b8ce13f2..3791ce36 100755
Binary files a/bin/composer.phar and b/bin/composer.phar differ
diff --git a/bin/gpm b/bin/gpm
index 592bb833..a2eebc68 100755
--- a/bin/gpm
+++ b/bin/gpm
@@ -1,9 +1,14 @@
#!/usr/bin/env php
arguments->add([
- 'environment' => [
- 'prefix' => 'e',
- 'longPrefix' => 'env',
- 'description' => 'Configuration Environment',
- 'defaultValue' => 'localhost'
- ]
-]);
-$climate->arguments->parse();
-
-// Set up environment based on params.
-$environment = $climate->arguments->get('environment');
-
$grav = Grav::instance(array('loader' => $autoload));
-$grav->setup($environment);
-
-$grav['config']->init();
-$grav['uri']->init();
-$grav['accounts'];
-
-$app = new Application('Grav Package Manager', GRAV_VERSION);
-$app->addCommands(array(
- new \Grav\Console\Gpm\IndexCommand(),
- new \Grav\Console\Gpm\VersionCommand(),
- new \Grav\Console\Gpm\InfoCommand(),
- new \Grav\Console\Gpm\InstallCommand(),
- new \Grav\Console\Gpm\UninstallCommand(),
- new \Grav\Console\Gpm\UpdateCommand(),
- new \Grav\Console\Gpm\SelfupgradeCommand(),
- new \Grav\Console\Gpm\DirectInstallCommand(),
-));
+$app = new GpmApplication('Grav Package Manager', GRAV_VERSION);
$app->run();
diff --git a/bin/grav b/bin/grav
index b1e4c90e..f3f77efe 100755
--- a/bin/grav
+++ b/bin/grav
@@ -1,10 +1,14 @@
#!/usr/bin/env php
arguments->add([
- 'environment' => [
- 'prefix' => 'e',
- 'longPrefix' => 'env',
- 'description' => 'Configuration Environment',
- 'defaultValue' => 'localhost'
- ]
-]);
-$climate->arguments->parse();
-
-// Set up environment based on params.
-$environment = $climate->arguments->get('environment');
-
$grav = Grav::instance(array('loader' => $autoload));
-$grav->setup($environment);
if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
-$app = new Application('Grav CLI Application', GRAV_VERSION);
-$app->addCommands(array(
- new \Grav\Console\Cli\InstallCommand(),
- new \Grav\Console\Cli\ComposerCommand(),
- new \Grav\Console\Cli\SandboxCommand(),
- new \Grav\Console\Cli\CleanCommand(),
- new \Grav\Console\Cli\ClearCacheCommand(),
- new \Grav\Console\Cli\BackupCommand(),
- new \Grav\Console\Cli\NewProjectCommand(),
- new \Grav\Console\Cli\SchedulerCommand(),
- new \Grav\Console\Cli\SecurityCommand(),
- new \Grav\Console\Cli\LogViewerCommand(),
- new \Grav\Console\Cli\YamlLinterCommand(),
-));
+$app = new GravApplication('Grav CLI Application', GRAV_VERSION);
$app->run();
diff --git a/bin/plugin b/bin/plugin
index 737eb910..b401c035 100755
--- a/bin/plugin
+++ b/bin/plugin
@@ -1,13 +1,14 @@
#!/usr/bin/env php
arguments->add([
- 'environment' => [
- 'prefix' => 'e',
- 'longPrefix' => 'env',
- 'description' => 'Configuration Environment',
- 'defaultValue' => 'localhost'
- ]
-]);
-$climate->arguments->parse();
-
-$environment = $climate->arguments->get('environment');
-
+// Bootstrap Grav container.
$grav = Grav::instance(array('loader' => $autoload));
-$grav->setup($environment);
-$grav->initializeCli();
-
-$app = new Application('Grav Plugins Commands', GRAV_VERSION);
-$pattern = '([A-Z]\w+Command\.php)';
-
-// get arguments and strip the application name
-if (null === $argv) {
- $argv = $_SERVER['argv'];
-}
-
-$bin = array_shift($argv);
-$name = array_shift($argv);
-$argv = array_merge([$bin], $argv);
-
-$input = new ArgvInput($argv);
-
-/** @var \Grav\Common\Data\Data $plugin */
-$plugin = $grav['plugins']->get($name);
-
-$output = new ConsoleOutput();
-$output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
-$output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
-
-if (!$name) {
- $output->writeln('');
- $output->writeln('Usage: ');
- $output->writeln(" {$bin} [slug] [command] [arguments]");
- $output->writeln('');
- $output->writeln('Example: ');
- $output->writeln(" {$bin} error log -l 1 --trace");
- $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
-
- $total = 0;
- if (count($list)) {
- $available = [];
- $output->writeln('');
- $output->writeln('Plugins with CLI available: ');
- foreach ($list as $index => $entry) {
- $split = explode('/', $entry);
- $entry = array_shift($split);
- $index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT);
- $total = str_pad($total++ + 1, 2, '0', STR_PAD_LEFT);
- if (\in_array($entry, $available, true)) {
- $total--;
- continue;
- }
-
- $available[] = $entry;
- $commands_count = $index - $total + 1;
- $output->writeln(' ' . $total . '. ' . str_pad($entry, 15) . " {$bin} {$entry} list ");
- }
- }
-
- exit;
-} else {
- if (is_null($plugin)) {
- $output->writeln('');
- $output->writeln("$name plugin not found ");
- die;
- }
-
- if (!$plugin->enabled) {
- $output->writeln('');
- $output->writeln("$name not enabled ");
- die;
- }
-}
-
-if ($plugin === null) {
- $output->writeln("Grav Plugin '{$name}' is not installed ");
- exit;
-}
-
-$path = 'plugins://' . $name . '/cli';
-
-try {
- $commands = Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm', 'levels' => 1]);
-} catch (\RuntimeException $e) {
- $output->writeln("No Console Commands for '{$name}' where found in '{$path}' ");
- exit;
-}
-
-foreach ($commands as $command_path) {
- $full_path = $grav['locator']->findResource("plugins://{$name}/cli/{$command_path}");
- require_once $full_path;
-
- $command_class = 'Grav\Plugin\Console\\' . preg_replace('/.php$/', '', $command_path);
- $command = new $command_class();
- $app->add($command);
-}
-$app->run($input);
+$app = new PluginApplication('Grav Plugins Commands', GRAV_VERSION);
+$app->run();
diff --git a/composer.json b/composer.json
index e6aa0226..e86c6aa0 100644
--- a/composer.json
+++ b/composer.json
@@ -12,55 +12,72 @@
"homepage": "https://getgrav.org",
"license": "MIT",
"require": {
- "php": ">=7.1.3",
+ "php": "^7.3.6 || ^8.0",
"ext-json": "*",
- "ext-mbstring": "*",
"ext-openssl": "*",
"ext-curl": "*",
"ext-zip": "*",
"ext-dom": "*",
- "symfony/polyfill-iconv": "^1.9",
- "symfony/polyfill-php72": "^1.9",
- "symfony/polyfill-php73": "^1.9",
+ "ext-libxml": "*",
+ "symfony/polyfill-mbstring": "~1.20",
+ "symfony/polyfill-iconv": "^1.20",
+ "symfony/polyfill-php74": "^1.20",
+ "symfony/polyfill-php80": "^1.20",
"psr/simple-cache": "^1.0",
"psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0",
- "kodus/psr7-server": "*",
- "nyholm/psr7": "^1.0",
- "twig/twig": "~1.40",
- "symfony/yaml": "~4.2.0",
- "symfony/console": "~4.2.0",
- "symfony/event-dispatcher": "~4.2.0",
- "symfony/var-dumper": "~4.2.0",
- "symfony/process": "~4.2.0",
- "doctrine/cache": "^1.8",
- "doctrine/collections": "^1.5",
- "guzzlehttp/psr7": "^1.4",
- "filp/whoops": "~2.2",
+ "psr/container": "~1.0.0",
+ "nyholm/psr7-server": "^1.0",
+ "nyholm/psr7": "^1.3",
+ "twig/twig": "~1.44",
+ "erusev/parsedown": "^1.7",
+ "erusev/parsedown-extra": "~0.8",
+ "symfony/contracts": "~1.1",
+ "symfony/yaml": "~4.4",
+ "symfony/console": "~4.4",
+ "symfony/event-dispatcher": "~4.4",
+ "symfony/var-dumper": "~4.4",
+ "symfony/process": "~4.4",
+ "doctrine/cache": "^1.10",
+ "doctrine/collections": "^1.6",
+ "guzzlehttp/psr7": "^1.7",
+ "filp/whoops": "~2.9",
"matthiasmullie/minify": "^1.3",
- "monolog/monolog": "~1.0",
- "gregwar/image": "2.*",
- "donatj/phpuseragentparser": "~1.0",
- "pimple/pimple": "~3.2",
- "rockettheme/toolbox": "~1.4.0",
- "maximebf/debugbar": "~1.15",
- "league/climate": "^3.4",
+ "monolog/monolog": "~1.25",
+ "getgrav/image": "^3.0",
+ "getgrav/cache": "^2.0",
+ "donatj/phpuseragentparser": "~1.1",
+ "pimple/pimple": "~3.3.0",
+ "rockettheme/toolbox": "~1.5",
+ "maximebf/debugbar": "~1.16",
+ "league/climate": "^3.6",
"antoligy/dom-string-iterators": "^1.0",
- "miljar/php-exif": "^0.6.4",
- "composer/ca-bundle": "^1.0",
+ "miljar/php-exif": "^0.6",
+ "composer/ca-bundle": "^1.2",
"dragonmantank/cron-expression": "^1.2",
- "phive/twig-extensions-deferred": "^1.0",
- "willdurand/negotiation": "2.x-dev"
+ "willdurand/negotiation": "^3.0",
+ "itsgoingd/clockwork": "^5.0",
+ "symfony/http-client": "^4.4",
+ "composer/semver": "^1.4",
+ "rhukster/dom-sanitizer": "^1.0"
},
"require-dev": {
- "codeception/codeception": "^2.4",
- "phpstan/phpstan": "^0.11",
- "phpstan/phpstan-deprecation-rules": "^0.11.0",
- "phpunit/php-code-coverage": "~6.0",
- "fzaninotto/faker": "^1.8",
- "victorjonsson/markdowndocs": "dev-master"
+ "codeception/codeception": "^4.1",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-deprecation-rules": "^0.12",
+ "phpunit/php-code-coverage": "~9.2",
+ "getgrav/markdowndocs": "^2.0",
+ "codeception/module-asserts": "^1.3",
+ "codeception/module-phpbrowser": "^1.0",
+ "symfony/service-contracts": "*"
+ },
+ "replace": {
+ "symfony/polyfill-php72": "*",
+ "symfony/polyfill-php73": "*"
},
"suggest": {
+ "ext-mbstring": "Recommended for better performance",
+ "ext-iconv": "Recommended for better performance",
"ext-zend-opcache": "Recommended for better performance",
"ext-intl": "Recommended for multi-language sites",
"ext-memcache": "Needed to support Memcache servers",
@@ -70,22 +87,16 @@
"config": {
"apcu-autoloader": true,
"platform": {
- "php": "7.1.3"
+ "php": "7.3.6"
}
},
- "repositories": [
- {
- "type": "vcs",
- "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator"
- }
- ],
"autoload": {
"psr-4": {
- "Grav\\": "system/src/Grav"
+ "Grav\\": "system/src/Grav",
+ "Twig\\": "system/src/Twig"
},
"files": [
- "system/defines.php",
- "system/aliases.php"
+ "system/defines.php"
]
},
"archive": {
@@ -94,12 +105,11 @@
]
},
"scripts": {
- "api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md",
- "api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md",
+ "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
"post-create-project-cmd": "bin/grav install",
- "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
- "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
- "phpstan-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M",
+ "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=520M system/src",
+ "phpstan-framework": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
+ "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins",
"test": "vendor/bin/codecept run unit",
"test-windows": "vendor\\bin\\codecept run unit"
},
diff --git a/composer.lock b/composer.lock
index 10f35c01..68dc24c9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "40dbb242443086c99aa47ea8ca366d67",
+ "content-hash": "23dd68cea2a3f2d963e57638131f1122",
"packages": [
{
"name": "antoligy/dom-string-iterators",
@@ -48,20 +48,24 @@
}
],
"description": "Composer package for DOMWordsIterator and DOMLettersIterator",
+ "support": {
+ "issues": "https://github.com/antoligy/dom-string-iterators/issues",
+ "source": "https://github.com/antoligy/dom-string-iterators/tree/v1.0.1"
+ },
"time": "2018-02-03T16:01:11+00:00"
},
{
"name": "composer/ca-bundle",
- "version": "1.2.6",
+ "version": "1.2.10",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e"
+ "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/47fe531de31fca4a1b997f87308e7d7804348f7e",
- "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/9fdb22c2e97a614657716178093cd1da90a64aa8",
+ "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8",
"shasum": ""
},
"require": {
@@ -70,14 +74,15 @@
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
+ "phpstan/phpstan": "^0.12.55",
"psr/log": "^1.0",
+ "symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-main": "1.x-dev"
}
},
"autoload": {
@@ -104,44 +109,142 @@
"ssl",
"tls"
],
- "time": "2020-01-13T10:02:55+00:00"
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/ca-bundle/issues",
+ "source": "https://github.com/composer/ca-bundle/tree/1.2.10"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-07T13:58:28+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "1.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "647490bbcaf7fc4891c58f47b825eb99d19c377a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/647490bbcaf7fc4891c58f47b825eb99d19c377a",
+ "reference": "647490bbcaf7fc4891c58f47b825eb99d19c377a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.5 || ^5.0.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/1.7.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-12-03T15:47:16+00:00"
},
{
"name": "doctrine/cache",
- "version": "1.10.0",
+ "version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
- "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62"
+ "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62",
- "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/4cf401d14df219fa6f38b671f5493449151c9ad8",
+ "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8",
"shasum": ""
},
"require": {
- "php": "~7.1"
+ "php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
- "doctrine/coding-standard": "^6.0",
+ "cache/integration-tests": "dev-master",
+ "doctrine/coding-standard": "^8.0",
"mongodb/mongodb": "^1.1",
- "phpunit/phpunit": "^7.0",
- "predis/predis": "~1.0"
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "predis/predis": "~1.0",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0",
+ "symfony/cache": "^4.4 || ^5.2 || ^6.0@dev",
+ "symfony/var-exporter": "^4.4 || ^5.2 || ^6.0@dev"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.9.x-dev"
- }
- },
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
@@ -186,37 +289,50 @@
"redis",
"xcache"
],
- "time": "2019-11-29T15:36:20+00:00"
+ "support": {
+ "issues": "https://github.com/doctrine/cache/issues",
+ "source": "https://github.com/doctrine/cache/tree/1.12.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-17T14:39:21+00:00"
},
{
"name": "doctrine/collections",
- "version": "1.6.4",
+ "version": "1.6.8",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
- "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7"
+ "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/collections/zipball/6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7",
- "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af",
+ "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af",
"shasum": ""
},
"require": {
- "php": "^7.1.3"
+ "php": "^7.1.3 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^6.0",
- "phpstan/phpstan-shim": "^0.9.2",
- "phpunit/phpunit": "^7.0",
- "vimeo/psalm": "^3.2.2"
+ "doctrine/coding-standard": "^9.0",
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5",
+ "vimeo/psalm": "^4.2.1"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.6.x-dev"
- }
- },
"autoload": {
"psr-4": {
"Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections"
@@ -256,29 +372,33 @@
"iterators",
"php"
],
- "time": "2019-11-13T13:07:11+00:00"
+ "support": {
+ "issues": "https://github.com/doctrine/collections/issues",
+ "source": "https://github.com/doctrine/collections/tree/1.6.8"
+ },
+ "time": "2021-08-10T18:51:53+00:00"
},
{
"name": "donatj/phpuseragentparser",
- "version": "v1.0.0",
+ "version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/donatj/PhpUserAgent.git",
- "reference": "1431382850017ac017d194f2a6f6cacb35212888"
+ "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/1431382850017ac017d194f2a6f6cacb35212888",
- "reference": "1431382850017ac017d194f2a6f6cacb35212888",
+ "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/cc9d872cddfc180c52d084d0dff1e4aad653d37f",
+ "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f",
"shasum": ""
},
"require": {
- "php": ">=5.3.0"
+ "php": ">=5.4.0"
},
"require-dev": {
"camspiers/json-pretty": "~1.0",
"donatj/drop": "*",
- "phpunit/phpunit": "~4.8"
+ "phpunit/phpunit": "~4.8|~9"
},
"type": "library",
"autoload": {
@@ -302,7 +422,7 @@
}
],
"description": "Lightning fast, minimalist PHP UserAgent string parser.",
- "homepage": "http://donatstudios.com/PHP-Parser-HTTP_USER_AGENT",
+ "homepage": "https://donatstudios.com/PHP-Parser-HTTP_USER_AGENT",
"keywords": [
"browser",
"browser detection",
@@ -310,9 +430,13 @@
"user agent",
"useragent"
],
+ "support": {
+ "issues": "https://github.com/donatj/PhpUserAgent/issues",
+ "source": "https://github.com/donatj/PhpUserAgent/tree/v1.5.0"
+ },
"funding": [
{
- "url": "https://www.paypal.me/donatj/15",
+ "url": "https://www.paypal.me/donatj/5",
"type": "custom"
},
{
@@ -320,7 +444,7 @@
"type": "github"
}
],
- "time": "2020-04-24T18:07:07+00:00"
+ "time": "2021-09-16T17:05:03+00:00"
},
{
"name": "dragonmantank/cron-expression",
@@ -364,29 +488,133 @@
"cron",
"schedule"
],
+ "support": {
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v1.2.0"
+ },
"time": "2017-01-23T04:29:33+00:00"
},
+ {
+ "name": "erusev/parsedown",
+ "version": "1.7.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown.git",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Parsedown": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "Parser for Markdown.",
+ "homepage": "http://parsedown.org",
+ "keywords": [
+ "markdown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown/issues",
+ "source": "https://github.com/erusev/parsedown/tree/1.7.x"
+ },
+ "time": "2019-12-30T22:54:17+00:00"
+ },
+ {
+ "name": "erusev/parsedown-extra",
+ "version": "0.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown-extra.git",
+ "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef",
+ "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef",
+ "shasum": ""
+ },
+ "require": {
+ "erusev/parsedown": "^1.7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "ParsedownExtra": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "An extension of Parsedown that adds support for Markdown Extra.",
+ "homepage": "https://github.com/erusev/parsedown-extra",
+ "keywords": [
+ "markdown",
+ "markdown extra",
+ "parsedown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown-extra/issues",
+ "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x"
+ },
+ "time": "2019-12-30T23:20:37+00:00"
+ },
{
"name": "filp/whoops",
- "version": "2.7.1",
+ "version": "2.14.3",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130"
+ "reference": "89584ce67dd32307f1063cc43846674f4679feda"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130",
- "reference": "fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/89584ce67dd32307f1063cc43846674f4679feda",
+ "reference": "89584ce67dd32307f1063cc43846674f4679feda",
"shasum": ""
},
"require": {
- "php": "^5.5.9 || ^7.0",
+ "php": "^5.5.9 || ^7.0 || ^8.0",
"psr/log": "^1.0.1"
},
"require-dev": {
"mockery/mockery": "^0.9 || ^1.0",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
"symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
},
"suggest": {
@@ -396,7 +624,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.6-dev"
+ "dev-master": "2.7-dev"
}
},
"autoload": {
@@ -425,21 +653,31 @@
"throwable",
"whoops"
],
- "time": "2020-01-15T10:00:00+00:00"
+ "support": {
+ "issues": "https://github.com/filp/whoops/issues",
+ "source": "https://github.com/filp/whoops/tree/2.14.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/denis-sokolov",
+ "type": "github"
+ }
+ ],
+ "time": "2021-09-19T12:00:00+00:00"
},
{
- "name": "gregwar/cache",
- "version": "v1.0.12",
+ "name": "getgrav/cache",
+ "version": "v2.0.0",
"target-dir": "Gregwar/Cache",
"source": {
"type": "git",
- "url": "https://github.com/Gregwar/Cache.git",
- "reference": "305d0f5a12c0beecbbd7e1de236f59f39e0c0ac3"
+ "url": "https://github.com/getgrav/Cache.git",
+ "reference": "56fd63f752779928fcd1074ab7d12f406dde8861"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Gregwar/Cache/zipball/305d0f5a12c0beecbbd7e1de236f59f39e0c0ac3",
- "reference": "305d0f5a12c0beecbbd7e1de236f59f39e0c0ac3",
+ "url": "https://api.github.com/repos/getgrav/Cache/zipball/56fd63f752779928fcd1074ab7d12f406dde8861",
+ "reference": "56fd63f752779928fcd1074ab7d12f406dde8861",
"shasum": ""
},
"require": {
@@ -459,6 +697,11 @@
{
"name": "Gregwar",
"email": "g.passault@gmail.com"
+ },
+ {
+ "name": "Grav CMS",
+ "email": "hello@getgrav.org",
+ "homepage": "https://getgrav.org"
}
],
"description": "A lightweight file-system cache system",
@@ -468,27 +711,30 @@
"file-system",
"system"
],
- "time": "2016-09-23T08:16:04+00:00"
+ "support": {
+ "source": "https://github.com/getgrav/Cache/tree/v2.0.0"
+ },
+ "time": "2021-04-20T05:48:00+00:00"
},
{
- "name": "gregwar/image",
- "version": "v2.0.25",
+ "name": "getgrav/image",
+ "version": "v3.0.0",
"target-dir": "Gregwar/Image",
"source": {
"type": "git",
- "url": "https://github.com/Gregwar/Image.git",
- "reference": "03534d5760cbea5c96e6292041ff81a3bb205c36"
+ "url": "https://github.com/getgrav/Image.git",
+ "reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Gregwar/Image/zipball/03534d5760cbea5c96e6292041ff81a3bb205c36",
- "reference": "03534d5760cbea5c96e6292041ff81a3bb205c36",
+ "url": "https://api.github.com/repos/getgrav/Image/zipball/02c1bb2c179dd894c4f6610c9c49da364ee7d264",
+ "reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264",
"shasum": ""
},
"require": {
"ext-gd": "*",
- "gregwar/cache": "^1.0.6",
- "php": "^5.3 || ^7.0"
+ "getgrav/cache": "^2.0",
+ "php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"sllh/php-cs-fixer-styleci-bridge": "~1.0",
@@ -512,6 +758,11 @@
"name": "Grégoire Passault",
"email": "g.passault@gmail.com",
"homepage": "http://www.gregwar.com/"
+ },
+ {
+ "name": "Grav CMS",
+ "email": "hello@getgrav.org",
+ "homepage": "https://getgrav.org"
}
],
"description": "Image handling",
@@ -520,20 +771,23 @@
"gd",
"image"
],
- "time": "2019-03-01T15:55:29+00:00"
+ "support": {
+ "source": "https://github.com/getgrav/Image/tree/v3.0.0"
+ },
+ "time": "2021-04-20T05:50:18+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "1.6.1",
+ "version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+ "reference": "dc960a912984efb74d0a90222870c72c87f10c91"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
+ "reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": ""
},
"require": {
@@ -546,15 +800,15 @@
},
"require-dev": {
"ext-zlib": "*",
- "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
- "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
@@ -591,39 +845,45 @@
"uri",
"url"
],
- "time": "2019-07-01T23:21:34+00:00"
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/1.8.2"
+ },
+ "time": "2021-04-26T09:17:50+00:00"
},
{
- "name": "kodus/psr7-server",
- "version": "1.0.1",
+ "name": "itsgoingd/clockwork",
+ "version": "v5.1.0",
"source": {
"type": "git",
- "url": "https://github.com/kodus/psr7-server.git",
- "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278"
+ "url": "https://github.com/itsgoingd/clockwork.git",
+ "reference": "b963dee47429a49c9669981cfa9a8362ce209278"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/kodus/psr7-server/zipball/dcfd0116451b0f0e7c6b23b831757ed288347278",
- "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278",
+ "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/b963dee47429a49c9669981cfa9a8362ce209278",
+ "reference": "b963dee47429a49c9669981cfa9a8362ce209278",
"shasum": ""
},
"require": {
- "php": "^7.1",
- "psr/http-factory": "^1.0",
- "psr/http-message": "^1.0"
- },
- "replace": {
- "nyholm/psr7-server": "^0.3"
- },
- "require-dev": {
- "nyholm/nsa": "^1.1",
- "nyholm/psr7": "^1.0",
- "phpunit/phpunit": "^7.0"
+ "ext-json": "*",
+ "php": ">=5.6",
+ "psr/log": "1.*"
},
"type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
+ ],
+ "aliases": {
+ "Clockwork": "Clockwork\\Support\\Laravel\\Facade"
+ }
+ }
+ },
"autoload": {
"psr-4": {
- "Nyholm\\Psr7Server\\": "src/"
+ "Clockwork\\": "Clockwork/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -632,45 +892,57 @@
],
"authors": [
{
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com"
- },
- {
- "name": "Martijn van der Ven",
- "email": "martijn@vanderven.se"
+ "name": "itsgoingd",
+ "email": "itsgoingd@luzer.sk",
+ "homepage": "https://twitter.com/itsgoingd"
}
],
- "description": "Helper classes to handle PSR-7 server requests",
- "homepage": "http://tnyholm.se",
+ "description": "php dev tools in your browser",
+ "homepage": "https://underground.works/clockwork",
"keywords": [
- "psr-17",
- "psr-7"
+ "Devtools",
+ "debugging",
+ "laravel",
+ "logging",
+ "lumen",
+ "profiling",
+ "slim"
+ ],
+ "support": {
+ "issues": "https://github.com/itsgoingd/clockwork/issues",
+ "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/itsgoingd",
+ "type": "github"
+ }
],
- "time": "2019-06-17T10:48:13+00:00"
+ "time": "2021-08-07T23:04:17+00:00"
},
{
"name": "league/climate",
- "version": "3.5.2",
+ "version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/climate.git",
- "reference": "6b53a28a58ad9f5f63042e291eb870cf0d02a9c9"
+ "reference": "5c717c3032c5118be7ad2395dbe0813d9894e8c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/climate/zipball/6b53a28a58ad9f5f63042e291eb870cf0d02a9c9",
- "reference": "6b53a28a58ad9f5f63042e291eb870cf0d02a9c9",
+ "url": "https://api.github.com/repos/thephpleague/climate/zipball/5c717c3032c5118be7ad2395dbe0813d9894e8c7",
+ "reference": "5c717c3032c5118be7ad2395dbe0813d9894e8c7",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": "^7.3 || ^8.0",
"psr/log": "^1.0",
"seld/cli-prompt": "^1.0"
},
"require-dev": {
"mikey179/vfsstream": "^1.4",
- "mockery/mockery": "^1.0",
- "phpunit/phpunit": "^5.7.16"
+ "mockery/mockery": "^1.4.2",
+ "phpunit/phpunit": "^9.5.0"
},
"suggest": {
"ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
@@ -707,20 +979,24 @@
"php",
"terminal"
],
- "time": "2019-12-01T15:25:43+00:00"
+ "support": {
+ "issues": "https://github.com/thephpleague/climate/issues",
+ "source": "https://github.com/thephpleague/climate/tree/3.7.0"
+ },
+ "time": "2021-01-10T20:18:52+00:00"
},
{
"name": "matthiasmullie/minify",
- "version": "1.3.63",
+ "version": "1.3.66",
"source": {
"type": "git",
"url": "https://github.com/matthiasmullie/minify.git",
- "reference": "9ba1b459828adc13430f4dd6c49dae4950dc4117"
+ "reference": "45fd3b0f1dfa2c965857c6d4a470bea52adc31a6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/9ba1b459828adc13430f4dd6c49dae4950dc4117",
- "reference": "9ba1b459828adc13430f4dd6c49dae4950dc4117",
+ "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/45fd3b0f1dfa2c965857c6d4a470bea52adc31a6",
+ "reference": "45fd3b0f1dfa2c965857c6d4a470bea52adc31a6",
"shasum": ""
},
"require": {
@@ -730,8 +1006,8 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.0",
- "matthiasmullie/scrapbook": "~1.0",
- "phpunit/phpunit": "~4.8"
+ "matthiasmullie/scrapbook": "dev-master",
+ "phpunit/phpunit": ">=4.8"
},
"suggest": {
"psr/cache-implementation": "Cache implementation to use with Minify::cache"
@@ -767,7 +1043,25 @@
"minifier",
"minify"
],
- "time": "2020-01-21T20:21:08+00:00"
+ "support": {
+ "issues": "https://github.com/matthiasmullie/minify/issues",
+ "source": "https://github.com/matthiasmullie/minify/tree/1.3.66"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/[user1",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/matthiasmullie] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g.",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/user2",
+ "type": "github"
+ }
+ ],
+ "time": "2021-01-06T15:18:10+00:00"
},
{
"name": "matthiasmullie/path-converter",
@@ -816,29 +1110,33 @@
"paths",
"relative"
],
+ "support": {
+ "issues": "https://github.com/matthiasmullie/path-converter/issues",
+ "source": "https://github.com/matthiasmullie/path-converter/tree/1.1.3"
+ },
"time": "2019-02-05T23:41:09+00:00"
},
{
"name": "maximebf/debugbar",
- "version": "v1.16.1",
+ "version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
- "reference": "58998b818c6567fac01e35b8a4b70c1a64530556"
+ "reference": "0a3532556be0145603f8a9de23e76dc28eed7054"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/58998b818c6567fac01e35b8a4b70c1a64530556",
- "reference": "58998b818c6567fac01e35b8a4b70c1a64530556",
+ "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/0a3532556be0145603f8a9de23e76dc28eed7054",
+ "reference": "0a3532556be0145603f8a9de23e76dc28eed7054",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": "^7.1|^8",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3|^4|^5"
},
"require-dev": {
- "phpunit/phpunit": "^5"
+ "phpunit/phpunit": "^7.5.20 || ^9.4.2"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
@@ -848,7 +1146,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.16-dev"
+ "dev-master": "1.17-dev"
}
},
"autoload": {
@@ -877,7 +1175,11 @@
"debug",
"debugbar"
],
- "time": "2019-11-24T09:46:11+00:00"
+ "support": {
+ "issues": "https://github.com/maximebf/php-debugbar/issues",
+ "source": "https://github.com/maximebf/php-debugbar/tree/v1.17.1"
+ },
+ "time": "2021-08-01T09:19:02+00:00"
},
{
"name": "miljar/php-exif",
@@ -933,20 +1235,24 @@
"jpeg",
"tiff"
],
+ "support": {
+ "issues": "https://github.com/PHPExif/php-exif/issues",
+ "source": "https://github.com/PHPExif/php-exif/tree/v0.6.5"
+ },
"time": "2019-02-11T13:47:52+00:00"
},
{
"name": "monolog/monolog",
- "version": "1.25.3",
+ "version": "1.26.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1"
+ "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1",
- "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+ "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
"shasum": ""
},
"require": {
@@ -960,11 +1266,10 @@
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
"graylog2/gelf-php": "~1.0",
- "jakub-onderka/php-parallel-lint": "0.9",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
+ "phpstan/phpstan": "^0.12.59",
"phpunit/phpunit": "~4.5",
- "phpunit/phpunit-mock-objects": "2.3.0",
"ruflin/elastica": ">=0.90 <3.0",
"sentry/sentry": "^0.13",
"swiftmailer/swiftmailer": "^5.3|^6.0"
@@ -983,11 +1288,6 @@
"sentry/sentry": "Allow sending log messages to a Sentry server"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
@@ -1011,24 +1311,38 @@
"logging",
"psr-3"
],
- "time": "2019-12-20T14:15:16+00:00"
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/1.26.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-05-28T08:32:12+00:00"
},
{
"name": "nyholm/psr7",
- "version": "1.2.1",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/Nyholm/psr7.git",
- "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c"
+ "reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Nyholm/psr7/zipball/55ff6b76573f5b242554c9775792bd59fb52e11c",
- "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c",
+ "url": "https://api.github.com/repos/Nyholm/psr7/zipball/2212385b47153ea71b1c1b1374f8cb5e4f7892ec",
+ "reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": ">=7.1",
"php-http/message-factory": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0"
@@ -1038,14 +1352,15 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
- "http-interop/http-factory-tests": "dev-master",
- "php-http/psr7-integration-tests": "dev-master",
- "phpunit/phpunit": "^7.5"
+ "http-interop/http-factory-tests": "^0.9",
+ "php-http/psr7-integration-tests": "^1.0",
+ "phpunit/phpunit": "^7.5 || 8.5 || 9.4",
+ "symfony/error-handler": "^4.4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "1.4-dev"
}
},
"autoload": {
@@ -1068,34 +1383,55 @@
}
],
"description": "A fast PHP7 implementation of PSR-7",
- "homepage": "http://tnyholm.se",
+ "homepage": "https://tnyholm.se",
"keywords": [
"psr-17",
"psr-7"
],
- "time": "2019-09-05T13:24:16+00:00"
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7/issues",
+ "source": "https://github.com/Nyholm/psr7/tree/1.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
+ ],
+ "time": "2021-07-02T08:32:20+00:00"
},
{
- "name": "phive/twig-extensions-deferred",
- "version": "v1.0.2",
+ "name": "nyholm/psr7-server",
+ "version": "1.0.2",
"source": {
"type": "git",
- "url": "https://github.com/rybakit/twig-deferred-extension.git",
- "reference": "5a2426d622afa74034e754ca5ea1d1ff7887627f"
+ "url": "https://github.com/Nyholm/psr7-server.git",
+ "reference": "b846a689844cef114e8079d8c80f0afd96745ae3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rybakit/twig-deferred-extension/zipball/5a2426d622afa74034e754ca5ea1d1ff7887627f",
- "reference": "5a2426d622afa74034e754ca5ea1d1ff7887627f",
+ "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/b846a689844cef114e8079d8c80f0afd96745ae3",
+ "reference": "b846a689844cef114e8079d8c80f0afd96745ae3",
"shasum": ""
},
"require": {
- "twig/twig": "~1.18"
+ "php": "^7.1 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "nyholm/nsa": "^1.1",
+ "nyholm/psr7": "^1.3",
+ "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3"
},
"type": "library",
"autoload": {
"psr-4": {
- "Phive\\Twig\\Extensions\\Deferred\\": "src/"
+ "Nyholm\\Psr7Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1104,19 +1440,35 @@
],
"authors": [
{
- "name": "Eugene Leonovich",
- "email": "gen.work@gmail.com"
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
}
],
- "description": "An extension for Twig that allows to defer block rendering",
- "homepage": "https://github.com/rybakit/twig-extensions-deferred",
+ "description": "Helper classes to handle PSR-7 server requests",
+ "homepage": "http://tnyholm.se",
"keywords": [
- "defer",
- "extension",
- "lazy",
- "twig"
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7-server/issues",
+ "source": "https://github.com/Nyholm/psr7-server/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
],
- "time": "2017-03-17T21:39:21+00:00"
+ "time": "2021-05-12T11:11:27+00:00"
},
{
"name": "php-http/message-factory",
@@ -1166,33 +1518,37 @@
"stream",
"uri"
],
+ "support": {
+ "issues": "https://github.com/php-http/message-factory/issues",
+ "source": "https://github.com/php-http/message-factory/tree/master"
+ },
"time": "2015-12-19T14:08:53+00:00"
},
{
"name": "pimple/pimple",
- "version": "v3.2.3",
+ "version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/silexphp/Pimple.git",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
+ "reference": "21e45061c3429b1e06233475cc0e1f6fc774d5b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
+ "url": "https://api.github.com/repos/silexphp/Pimple/zipball/21e45061c3429b1e06233475cc0e1f6fc774d5b0",
+ "reference": "21e45061c3429b1e06233475cc0e1f6fc774d5b0",
"shasum": ""
},
"require": {
- "php": ">=5.3.0",
+ "php": ">=7.2.5",
"psr/container": "^1.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "^3.2"
+ "symfony/phpunit-bridge": "^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.2.x-dev"
+ "dev-master": "3.3.x-dev"
}
},
"autoload": {
@@ -1211,12 +1567,15 @@
}
],
"description": "Pimple, a simple Dependency Injection Container",
- "homepage": "http://pimple.sensiolabs.org",
+ "homepage": "https://pimple.symfony.com",
"keywords": [
"container",
"dependency injection"
],
- "time": "2018-01-21T07:42:36+00:00"
+ "support": {
+ "source": "https://github.com/silexphp/Pimple/tree/v3.3.1"
+ },
+ "time": "2020-11-24T20:35:42+00:00"
},
{
"name": "psr/cache",
@@ -1262,6 +1621,9 @@
"psr",
"psr-6"
],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/master"
+ },
"time": "2016-08-06T20:24:11+00:00"
},
{
@@ -1311,6 +1673,10 @@
"container-interop",
"psr"
],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/master"
+ },
"time": "2017-02-14T16:28:37+00:00"
},
{
@@ -1363,6 +1729,9 @@
"request",
"response"
],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/master"
+ },
"time": "2019-04-30T12:38:16+00:00"
},
{
@@ -1413,6 +1782,9 @@
"request",
"response"
],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
"time": "2016-08-06T14:39:51+00:00"
},
{
@@ -1466,6 +1838,10 @@
"response",
"server"
],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-handler/issues",
+ "source": "https://github.com/php-fig/http-server-handler/tree/master"
+ },
"time": "2018-10-30T16:46:14+00:00"
},
{
@@ -1519,20 +1895,24 @@
"request",
"response"
],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-middleware/issues",
+ "source": "https://github.com/php-fig/http-server-middleware/tree/master"
+ },
"time": "2018-10-30T17:12:04+00:00"
},
{
"name": "psr/log",
- "version": "1.1.2",
+ "version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
@@ -1556,7 +1936,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@@ -1566,7 +1946,10 @@
"psr",
"psr-3"
],
- "time": "2019-11-01T11:05:21+00:00"
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
},
{
"name": "psr/simple-cache",
@@ -1614,6 +1997,9 @@
"psr-16",
"simple-cache"
],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
"time": "2017-10-23T01:57:42+00:00"
},
{
@@ -1654,31 +2040,82 @@
}
],
"description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
"time": "2019-03-08T08:55:37+00:00"
},
+ {
+ "name": "rhukster/dom-sanitizer",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rhukster/dom-sanitizer.git",
+ "reference": "836d1dd9808adee0e4bb0ce4cdacc0f9a1497b7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/836d1dd9808adee0e4bb0ce4cdacc0f9a1497b7e",
+ "reference": "836d1dd9808adee0e4bb0ce4cdacc0f9a1497b7e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Rhukster\\DomSanitizer\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Andy Miller",
+ "email": "rhuk@rhuk.net"
+ }
+ ],
+ "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+",
+ "support": {
+ "issues": "https://github.com/rhukster/dom-sanitizer/issues",
+ "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.5"
+ },
+ "time": "2021-09-29T20:14:54+00:00"
+ },
{
"name": "rockettheme/toolbox",
- "version": "1.4.7",
+ "version": "1.5.10",
"source": {
"type": "git",
"url": "https://github.com/rockettheme/toolbox.git",
- "reference": "6a86bc0607884d2194260b6b72d67333e0141585"
+ "reference": "d9738de013fa12df77754a0f11dded220b246efb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/6a86bc0607884d2194260b6b72d67333e0141585",
- "reference": "6a86bc0607884d2194260b6b72d67333e0141585",
+ "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/d9738de013fa12df77754a0f11dded220b246efb",
+ "reference": "d9738de013fa12df77754a0f11dded220b246efb",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": ">=5.4.0",
+ "php": ">=5.6.0",
"pimple/pimple": "~3.0",
- "symfony/event-dispatcher": ">2.5",
- "symfony/yaml": ">2.5"
+ "symfony/event-dispatcher": "^3.4|^4.0",
+ "symfony/yaml": "^3.4|^4.0"
},
"require-dev": {
- "phpunit/phpunit": "~6"
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-deprecation-rules": "^0.12",
+ "phpunit/phpunit": "~7.0"
},
"type": "library",
"autoload": {
@@ -1704,25 +2141,32 @@
"php",
"rockettheme"
],
- "time": "2020-03-19T18:24:40+00:00"
+ "support": {
+ "issues": "https://github.com/rockettheme/toolbox/issues",
+ "source": "https://github.com/rockettheme/toolbox/tree/1.5.10"
+ },
+ "time": "2021-09-29T16:50:13+00:00"
},
{
"name": "seld/cli-prompt",
- "version": "1.0.3",
+ "version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/cli-prompt.git",
- "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd"
+ "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/a19a7376a4689d4d94cab66ab4f3c816019ba8dd",
- "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd",
+ "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5",
+ "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.63"
+ },
"type": "library",
"extra": {
"branch-alias": {
@@ -1752,41 +2196,51 @@
"input",
"prompt"
],
- "time": "2017-03-18T11:32:45+00:00"
+ "support": {
+ "issues": "https://github.com/Seldaek/cli-prompt/issues",
+ "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4"
+ },
+ "time": "2020-12-15T21:32:01+00:00"
},
{
"name": "symfony/console",
- "version": "v4.2.12",
+ "version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "fc2e274aade6567a750551942094b2145ade9b6c"
+ "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/fc2e274aade6567a750551942094b2145ade9b6c",
- "reference": "fc2e274aade6567a750551942094b2145ade9b6c",
+ "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22",
+ "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
- "symfony/contracts": "^1.0",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": ">=7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/service-contracts": "^1.1|^2"
},
"conflict": {
+ "psr/log": ">=3",
"symfony/dependency-injection": "<3.4",
+ "symfony/event-dispatcher": "<4.3|>=5",
+ "symfony/lock": "<4.4",
"symfony/process": "<3.3"
},
"provide": {
- "psr/log-implementation": "1.0"
+ "psr/log-implementation": "1.0|2.0"
},
"require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "~3.4|~4.0",
- "symfony/dependency-injection": "~3.4|~4.0",
- "symfony/event-dispatcher": "~3.4|~4.0",
- "symfony/lock": "~3.4|~4.0",
- "symfony/process": "~3.4|~4.0"
+ "psr/log": "^1|^2",
+ "symfony/config": "^3.4|^4.0|^5.0",
+ "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/lock": "^4.4|^5.0",
+ "symfony/process": "^3.4|^4.0|^5.0",
+ "symfony/var-dumper": "^4.3|^5.0"
},
"suggest": {
"psr/log": "For using the console logger",
@@ -1795,11 +2249,6 @@
"symfony/process": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
@@ -1822,26 +2271,43 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Console Component",
+ "description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
- "time": "2019-07-24T17:13:20+00:00"
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v4.4.30"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-08-25T19:27:26+00:00"
},
{
"name": "symfony/contracts",
- "version": "v1.1.8",
+ "version": "v1.1.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/contracts.git",
- "reference": "f51bca9de06b7a25b19a4155da7bebad099a5def"
+ "reference": "011c20407c4b99d454f44021d023fb39ce23b73d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/contracts/zipball/f51bca9de06b7a25b19a4155da7bebad099a5def",
- "reference": "f51bca9de06b7a25b19a4155da7bebad099a5def",
+ "url": "https://api.github.com/repos/symfony/contracts/zipball/011c20407c4b99d454f44021d023fb39ce23b73d",
+ "reference": "011c20407c4b99d454f44021d023fb39ce23b73d",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
+ "php": ">=7.1.3",
"psr/cache": "^1.0",
"psr/container": "^1.0"
},
@@ -1901,46 +2367,66 @@
"interoperability",
"standards"
],
- "time": "2019-11-07T12:44:51+00:00"
+ "support": {
+ "source": "https://github.com/symfony/contracts/tree/v1.1.10"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-09-02T16:08:58+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v4.2.12",
+ "version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "852548c7c704f14d2f6700c8d872a05bd2028732"
+ "reference": "2fe81680070043c4c80e7cedceb797e34f377bac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/852548c7c704f14d2f6700c8d872a05bd2028732",
- "reference": "852548c7c704f14d2f6700c8d872a05bd2028732",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac",
+ "reference": "2fe81680070043c4c80e7cedceb797e34f377bac",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
- "symfony/contracts": "^1.0"
+ "php": ">=7.1.3",
+ "symfony/event-dispatcher-contracts": "^1.1",
+ "symfony/polyfill-php80": "^1.16"
},
"conflict": {
"symfony/dependency-injection": "<3.4"
},
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "1.1"
+ },
"require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "~3.4|~4.0",
- "symfony/dependency-injection": "~3.4|~4.0",
- "symfony/expression-language": "~3.4|~4.0",
- "symfony/stopwatch": "~3.4|~4.0"
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^3.4|^4.0|^5.0",
+ "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+ "symfony/error-handler": "~3.4|~4.4",
+ "symfony/expression-language": "^3.4|^4.0|^5.0",
+ "symfony/http-foundation": "^3.4|^4.0|^5.0",
+ "symfony/service-contracts": "^1.1|^2",
+ "symfony/stopwatch": "^3.4|^4.0|^5.0"
},
"suggest": {
"symfony/dependency-injection": "",
"symfony/http-kernel": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
@@ -1963,26 +2449,124 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony EventDispatcher Component",
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-08-04T20:31:23+00:00"
+ },
+ {
+ "name": "symfony/http-client",
+ "version": "v4.4.31",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "6b900ffa399e25203f30f79f6f4a56b89eee14c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/6b900ffa399e25203f30f79f6f4a56b89eee14c2",
+ "reference": "6b900ffa399e25203f30f79f6f4a56b89eee14c2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3",
+ "psr/log": "^1|^2|^3",
+ "symfony/http-client-contracts": "^1.1.10|^2",
+ "symfony/polyfill-php73": "^1.11",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/service-contracts": "^1.0|^2"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "1.1|2.0"
+ },
+ "require-dev": {
+ "guzzlehttp/promises": "^1.4",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/dependency-injection": "^4.3|^5.0",
+ "symfony/http-kernel": "^4.4.13",
+ "symfony/process": "^4.2|^5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
- "time": "2019-06-26T06:46:55+00:00"
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v4.4.31"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-09-06T10:00:00+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.14.0",
+ "version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38"
+ "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
- "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+ "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
@@ -1990,7 +2574,11 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.14-dev"
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -2023,24 +2611,41 @@
"polyfill",
"portable"
],
- "time": "2020-01-13T11:15:53+00:00"
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-iconv",
- "version": "v1.14.0",
+ "version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
- "reference": "926832ce51059bb58211b7b2080a88e0c3b5328e"
+ "reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/926832ce51059bb58211b7b2080a88e0c3b5328e",
- "reference": "926832ce51059bb58211b7b2080a88e0c3b5328e",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/63b5bb7db83e5673936d6e3b8b3e022ff6474933",
+ "reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-iconv": "For best performance"
@@ -2048,7 +2653,11 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.14-dev"
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -2082,24 +2691,41 @@
"portable",
"shim"
],
- "time": "2020-01-13T11:15:53+00:00"
+ "support": {
+ "source": "https://github.com/symfony/polyfill-iconv/tree/v1.23.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.14.0",
+ "version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "34094cfa9abe1f0f14f48f490772db7a775559f2"
+ "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/34094cfa9abe1f0f14f48f490772db7a775559f2",
- "reference": "34094cfa9abe1f0f14f48f490772db7a775559f2",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
+ "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
@@ -2107,7 +2733,11 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.14-dev"
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -2141,34 +2771,55 @@
"portable",
"shim"
],
- "time": "2020-01-13T11:15:53+00:00"
- },
- {
- "name": "symfony/polyfill-php72",
- "version": "v1.14.0",
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-05-27T12:26:48+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php74",
+ "version": "v1.23.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf"
+ "url": "https://github.com/symfony/polyfill-php74.git",
+ "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf",
- "reference": "46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf",
+ "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/a5d80cdf049bd3b0af6da91184a2cd37533c0fd8",
+ "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.14-dev"
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Php72\\": ""
+ "Symfony\\Polyfill\\Php74\\": ""
},
"files": [
"bootstrap.php"
@@ -2179,6 +2830,10 @@
"MIT"
],
"authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
@@ -2188,7 +2843,7 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+ "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
@@ -2196,34 +2851,55 @@
"portable",
"shim"
],
- "time": "2020-01-13T11:15:53+00:00"
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php74/tree/v1.23.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-02-19T12:13:01+00:00"
},
{
- "name": "symfony/polyfill-php73",
- "version": "v1.14.0",
+ "name": "symfony/polyfill-php80",
+ "version": "v1.23.1",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "5e66a0fa1070bf46bec4bea7962d285108edd675"
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/5e66a0fa1070bf46bec4bea7962d285108edd675",
- "reference": "5e66a0fa1070bf46bec4bea7962d285108edd675",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
+ "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.14-dev"
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Php73\\": ""
+ "Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
@@ -2237,6 +2913,10 @@
"MIT"
],
"authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
@@ -2246,7 +2926,7 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
@@ -2254,31 +2934,44 @@
"portable",
"shim"
],
- "time": "2020-01-13T11:15:53+00:00"
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-28T13:41:28+00:00"
},
{
"name": "symfony/process",
- "version": "v4.2.12",
+ "version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2"
+ "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2",
- "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2",
+ "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
+ "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
"shasum": ""
},
"require": {
- "php": "^7.1.3"
+ "php": ">=7.1.3",
+ "symfony/polyfill-php80": "^1.16"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
@@ -2301,28 +2994,46 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Process Component",
+ "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
- "time": "2019-05-30T16:06:08+00:00"
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v4.4.30"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-08-04T20:31:23+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v4.2.12",
+ "version": "v4.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "4e18e041a477edbb8c54e053f179672f9413816c"
+ "reference": "1f12cc0c2e880a5f39575c19af81438464717839"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4e18e041a477edbb8c54e053f179672f9413816c",
- "reference": "4e18e041a477edbb8c54e053f179672f9413816c",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1f12cc0c2e880a5f39575c19af81438464717839",
+ "reference": "1f12cc0c2e880a5f39575c19af81438464717839",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
+ "php": ">=7.1.3",
"symfony/polyfill-mbstring": "~1.0",
- "symfony/polyfill-php72": "~1.5"
+ "symfony/polyfill-php72": "~1.5",
+ "symfony/polyfill-php80": "^1.16"
},
"conflict": {
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
@@ -2330,9 +3041,9 @@
},
"require-dev": {
"ext-iconv": "*",
- "symfony/console": "~3.4|~4.0",
- "symfony/process": "~3.4|~4.0",
- "twig/twig": "~1.34|~2.4"
+ "symfony/console": "^3.4|^4.0|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "twig/twig": "^1.43|^2.13|^3.0.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
@@ -2343,11 +3054,6 @@
"Resources/bin/var-dump-server"
],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
"autoload": {
"files": [
"Resources/functions/dump.php"
@@ -2373,47 +3079,59 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony mechanism for exploring and dumping PHP variables",
+ "description": "Provides mechanisms for walking through any arbitrary PHP variable",
"homepage": "https://symfony.com",
"keywords": [
"debug",
"dump"
],
- "time": "2019-07-27T06:42:33+00:00"
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v4.4.31"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-09-24T15:30:11+00:00"
},
{
"name": "symfony/yaml",
- "version": "v4.2.12",
+ "version": "v4.4.29",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "9468fef6f1c740b96935e9578560a9e9189ca154"
+ "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/9468fef6f1c740b96935e9578560a9e9189ca154",
- "reference": "9468fef6f1c740b96935e9578560a9e9189ca154",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
+ "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
+ "php": ">=7.1.3",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/console": "<3.4"
},
"require-dev": {
- "symfony/console": "~3.4|~4.0"
+ "symfony/console": "^3.4|^4.0|^5.0"
},
"suggest": {
"symfony/console": "For validating YAML files using the lint command"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
@@ -2436,36 +3154,53 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Yaml Component",
+ "description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
- "time": "2019-07-24T14:47:26+00:00"
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v4.4.29"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-27T16:19:30+00:00"
},
{
"name": "twig/twig",
- "version": "v1.42.5",
+ "version": "v1.44.5",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "87b2ea9d8f6fd014d0621ca089bb1b3769ea3f8e"
+ "reference": "dd4353357c5a116322e92a00d16043a31881a81e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/87b2ea9d8f6fd014d0621ca089bb1b3769ea3f8e",
- "reference": "87b2ea9d8f6fd014d0621ca089bb1b3769ea3f8e",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/dd4353357c5a116322e92a00d16043a31881a81e",
+ "reference": "dd4353357c5a116322e92a00d16043a31881a81e",
"shasum": ""
},
"require": {
- "php": ">=5.5.0",
+ "php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"psr/container": "^1.0",
- "symfony/phpunit-bridge": "^4.4|^5.0"
+ "symfony/phpunit-bridge": "^4.4.9|^5.0.9"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.42-dev"
+ "dev-master": "1.44-dev"
}
},
"autoload": {
@@ -2502,32 +3237,46 @@
"keywords": [
"templating"
],
- "time": "2020-02-11T05:59:23+00:00"
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v1.44.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-09-17T08:35:19+00:00"
},
{
"name": "willdurand/negotiation",
- "version": "2.x-dev",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/willdurand/Negotiation.git",
- "reference": "cf78c9ac47e8e1e141bf609c71e9e2c3a7780dcf"
+ "reference": "04e14f38d4edfcc974114a07d2777d90c98f3d9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/cf78c9ac47e8e1e141bf609c71e9e2c3a7780dcf",
- "reference": "cf78c9ac47e8e1e141bf609c71e9e2c3a7780dcf",
+ "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/04e14f38d4edfcc974114a07d2777d90c98f3d9c",
+ "reference": "04e14f38d4edfcc974114a07d2777d90c98f3d9c",
"shasum": ""
},
"require": {
- "php": ">=5.4.0"
+ "php": ">=7.1.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.5"
+ "symfony/phpunit-bridge": "^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.3-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -2554,31 +3303,36 @@
"header",
"negotiation"
],
- "time": "2017-08-04T15:54:30+00:00"
+ "support": {
+ "issues": "https://github.com/willdurand/Negotiation/issues",
+ "source": "https://github.com/willdurand/Negotiation/tree/3.0.0"
+ },
+ "time": "2020-09-25T08:01:41+00:00"
}
],
"packages-dev": [
{
"name": "behat/gherkin",
- "version": "v4.6.2",
+ "version": "v4.8.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/Gherkin.git",
- "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31"
+ "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31",
- "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31",
+ "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2391482cd003dfdc36b679b27e9f5326bd656acd",
+ "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd",
"shasum": ""
},
"require": {
- "php": ">=5.3.1"
+ "php": "~7.2|~8.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.5|~5",
- "symfony/phpunit-bridge": "~2.7|~3|~4",
- "symfony/yaml": "~2.3|~3|~4"
+ "cucumber/cucumber": "dev-gherkin-16.0.0",
+ "phpunit/phpunit": "~8|~9",
+ "symfony/phpunit-bridge": "~3|~4|~5",
+ "symfony/yaml": "~3|~4|~5"
},
"suggest": {
"symfony/yaml": "If you want to parse features, represented in YAML files"
@@ -2605,7 +3359,7 @@
"homepage": "http://everzet.com"
}
],
- "description": "Gherkin DSL parser for PHP 5.3",
+ "description": "Gherkin DSL parser for PHP",
"homepage": "http://behat.org/",
"keywords": [
"BDD",
@@ -2615,62 +3369,59 @@
"gherkin",
"parser"
],
- "time": "2020-03-17T14:03:26+00:00"
+ "support": {
+ "issues": "https://github.com/Behat/Gherkin/issues",
+ "source": "https://github.com/Behat/Gherkin/tree/v4.8.0"
+ },
+ "time": "2021-02-04T12:44:21+00:00"
},
{
"name": "codeception/codeception",
- "version": "2.5.6",
+ "version": "4.1.22",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Codeception.git",
- "reference": "b83a9338296e706fab2ceb49de8a352fbca3dc98"
+ "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b83a9338296e706fab2ceb49de8a352fbca3dc98",
- "reference": "b83a9338296e706fab2ceb49de8a352fbca3dc98",
+ "url": "https://api.github.com/repos/Codeception/Codeception/zipball/9777ec3690ceedc4bce2ed13af7af4ca4ee3088f",
+ "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f",
"shasum": ""
},
"require": {
"behat/gherkin": "^4.4.0",
- "codeception/phpunit-wrapper": "^6.0.9|^7.0.6",
- "codeception/stub": "^2.0",
+ "codeception/lib-asserts": "^1.0",
+ "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0",
+ "codeception/stub": "^2.0 | ^3.0",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "facebook/webdriver": ">=1.1.3 <2.0",
- "guzzlehttp/guzzle": ">=4.1.4 <7.0",
- "guzzlehttp/psr7": "~1.0",
- "php": ">=5.6.0 <8.0",
- "symfony/browser-kit": ">=2.7 <5.0",
- "symfony/console": ">=2.7 <5.0",
- "symfony/css-selector": ">=2.7 <5.0",
- "symfony/dom-crawler": ">=2.7 <5.0",
- "symfony/event-dispatcher": ">=2.7 <5.0",
- "symfony/finder": ">=2.7 <5.0",
- "symfony/yaml": ">=2.7 <5.0"
+ "guzzlehttp/psr7": "^1.4 | ^2.0",
+ "php": ">=5.6.0 <9.0",
+ "symfony/console": ">=2.7 <6.0",
+ "symfony/css-selector": ">=2.7 <6.0",
+ "symfony/event-dispatcher": ">=2.7 <6.0",
+ "symfony/finder": ">=2.7 <6.0",
+ "symfony/yaml": ">=2.7 <6.0"
},
"require-dev": {
+ "codeception/module-asserts": "1.*@dev",
+ "codeception/module-cli": "1.*@dev",
+ "codeception/module-db": "1.*@dev",
+ "codeception/module-filesystem": "1.*@dev",
+ "codeception/module-phpbrowser": "1.*@dev",
"codeception/specify": "~0.3",
- "facebook/graph-sdk": "~5.3",
- "flow/jsonpath": "~0.2",
+ "codeception/util-universalframework": "*@dev",
"monolog/monolog": "~1.8",
- "pda/pheanstalk": "~3.0",
- "php-amqplib/php-amqplib": "~2.4",
- "predis/predis": "^1.0",
"squizlabs/php_codesniffer": "~2.0",
- "symfony/process": ">=2.7 <5.0",
- "vlucas/phpdotenv": "^3.0"
+ "symfony/process": ">=2.7 <6.0",
+ "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0"
},
"suggest": {
- "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module",
- "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests",
"codeception/specify": "BDD-style code blocks",
"codeception/verify": "BDD-style assertions",
- "flow/jsonpath": "For using JSONPath in REST module",
- "league/factory-muffin": "For DataFactory module",
- "league/factory-muffin-faker": "For Faker support in DataFactory module",
- "phpseclib/phpseclib": "for SFTP option in FTP Module",
+ "hoa/console": "For interactive console functionality",
"stecman/symfony-console-completion": "For BASH autocompletion",
"symfony/phpunit-bridge": "For phpunit-bridge support"
},
@@ -2707,37 +3458,42 @@
"functional testing",
"unit testing"
],
- "time": "2019-04-24T11:28:19+00:00"
+ "support": {
+ "issues": "https://github.com/Codeception/Codeception/issues",
+ "source": "https://github.com/Codeception/Codeception/tree/4.1.22"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/codeception",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2021-08-06T17:15:34+00:00"
},
{
- "name": "codeception/phpunit-wrapper",
- "version": "7.8.0",
+ "name": "codeception/lib-asserts",
+ "version": "1.13.2",
"source": {
"type": "git",
- "url": "https://github.com/Codeception/phpunit-wrapper.git",
- "reference": "bc847bd4f8f6d09012543e2a856f19fe4ecdcf3a"
+ "url": "https://github.com/Codeception/lib-asserts.git",
+ "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/bc847bd4f8f6d09012543e2a856f19fe4ecdcf3a",
- "reference": "bc847bd4f8f6d09012543e2a856f19fe4ecdcf3a",
+ "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6",
+ "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6",
"shasum": ""
},
"require": {
- "phpunit/php-code-coverage": "^6.0",
- "phpunit/phpunit": "7.5.*",
- "sebastian/comparator": "^3.0",
- "sebastian/diff": "^3.0"
- },
- "require-dev": {
- "codeception/specify": "*",
- "vlucas/phpdotenv": "^3.0"
+ "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0",
+ "ext-dom": "*",
+ "php": ">=5.6.0 <9.0"
},
"type": "library",
"autoload": {
- "psr-4": {
- "Codeception\\PHPUnit\\": "src\\"
- }
+ "classmap": [
+ "src/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -2745,69 +3501,116 @@
],
"authors": [
{
- "name": "Davert",
- "email": "davert.php@resend.cc"
+ "name": "Michael Bodnarchuk",
+ "email": "davert@mail.ua",
+ "homepage": "http://codegyre.com"
+ },
+ {
+ "name": "Gintautas Miselis"
+ },
+ {
+ "name": "Gustavo Nieves",
+ "homepage": "https://medium.com/@ganieves"
}
],
- "description": "PHPUnit classes used by Codeception",
- "time": "2019-12-23T06:55:58+00:00"
+ "description": "Assertion methods used by Codeception core and Asserts module",
+ "homepage": "https://codeception.com/",
+ "keywords": [
+ "codeception"
+ ],
+ "support": {
+ "issues": "https://github.com/Codeception/lib-asserts/issues",
+ "source": "https://github.com/Codeception/lib-asserts/tree/1.13.2"
+ },
+ "time": "2020-10-21T16:26:20+00:00"
},
{
- "name": "codeception/stub",
- "version": "2.1.0",
+ "name": "codeception/lib-innerbrowser",
+ "version": "1.5.1",
"source": {
"type": "git",
- "url": "https://github.com/Codeception/Stub.git",
- "reference": "853657f988942f7afb69becf3fd0059f192c705a"
+ "url": "https://github.com/Codeception/lib-innerbrowser.git",
+ "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a",
- "reference": "853657f988942f7afb69becf3fd0059f192c705a",
+ "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2",
+ "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2",
"shasum": ""
},
"require": {
- "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3"
+ "codeception/codeception": "4.*@dev",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.6.0 <9.0",
+ "symfony/browser-kit": ">=2.7 <6.0",
+ "symfony/dom-crawler": ">=2.7 <6.0"
+ },
+ "conflict": {
+ "codeception/codeception": "<4.0"
+ },
+ "require-dev": {
+ "codeception/util-universalframework": "dev-master"
},
"type": "library",
"autoload": {
- "psr-4": {
- "Codeception\\": "src/"
- }
+ "classmap": [
+ "src/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "description": "Flexible Stub wrapper for PHPUnit's Mock Builder",
- "time": "2019-03-02T15:35:10+00:00"
+ "authors": [
+ {
+ "name": "Michael Bodnarchuk",
+ "email": "davert@mail.ua",
+ "homepage": "http://codegyre.com"
+ },
+ {
+ "name": "Gintautas Miselis"
+ }
+ ],
+ "description": "Parent library for all Codeception framework modules and PhpBrowser",
+ "homepage": "https://codeception.com/",
+ "keywords": [
+ "codeception"
+ ],
+ "support": {
+ "issues": "https://github.com/Codeception/lib-innerbrowser/issues",
+ "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1"
+ },
+ "time": "2021-08-30T15:21:42+00:00"
},
{
- "name": "composer/xdebug-handler",
- "version": "1.4.1",
+ "name": "codeception/module-asserts",
+ "version": "1.3.1",
"source": {
"type": "git",
- "url": "https://github.com/composer/xdebug-handler.git",
- "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7"
+ "url": "https://github.com/Codeception/module-asserts.git",
+ "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7",
- "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7",
+ "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de",
+ "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0 || ^8.0",
- "psr/log": "^1.0"
+ "codeception/codeception": "*@dev",
+ "codeception/lib-asserts": "^1.13.1",
+ "php": ">=5.6.0 <9.0"
},
- "require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
+ "conflict": {
+ "codeception/codeception": "<4.0"
},
"type": "library",
"autoload": {
- "psr-4": {
- "Composer\\XdebugHandler\\": "src"
- }
+ "classmap": [
+ "src/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -2815,59 +3618,63 @@
],
"authors": [
{
- "name": "John Stevenson",
- "email": "john-stevenson@blueyonder.co.uk"
+ "name": "Michael Bodnarchuk"
+ },
+ {
+ "name": "Gintautas Miselis"
+ },
+ {
+ "name": "Gustavo Nieves",
+ "homepage": "https://medium.com/@ganieves"
}
],
- "description": "Restarts a process without Xdebug.",
+ "description": "Codeception module containing various assertions",
+ "homepage": "https://codeception.com/",
"keywords": [
- "Xdebug",
- "performance"
- ],
- "funding": [
- {
- "url": "https://packagist.com",
- "type": "custom"
- }
+ "assertions",
+ "asserts",
+ "codeception"
],
- "time": "2020-03-01T12:26:26+00:00"
+ "support": {
+ "issues": "https://github.com/Codeception/module-asserts/issues",
+ "source": "https://github.com/Codeception/module-asserts/tree/1.3.1"
+ },
+ "time": "2020-10-21T16:48:15+00:00"
},
{
- "name": "doctrine/instantiator",
- "version": "1.3.0",
+ "name": "codeception/module-phpbrowser",
+ "version": "1.0.2",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "ae466f726242e637cebdd526a7d991b9433bacf1"
+ "url": "https://github.com/Codeception/module-phpbrowser.git",
+ "reference": "770a6be4160a5c0c08d100dd51bff35f6056bbf1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1",
- "reference": "ae466f726242e637cebdd526a7d991b9433bacf1",
+ "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/770a6be4160a5c0c08d100dd51bff35f6056bbf1",
+ "reference": "770a6be4160a5c0c08d100dd51bff35f6056bbf1",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "codeception/codeception": "^4.0",
+ "codeception/lib-innerbrowser": "^1.3",
+ "guzzlehttp/guzzle": "^6.3|^7.0",
+ "php": ">=5.6.0 <9.0"
+ },
+ "conflict": {
+ "codeception/codeception": "<4.0"
},
"require-dev": {
- "doctrine/coding-standard": "^6.0",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpbench/phpbench": "^0.13",
- "phpstan/phpstan-phpunit": "^0.11",
- "phpstan/phpstan-shim": "^0.11",
- "phpunit/phpunit": "^7.0"
+ "codeception/module-rest": "^1.0"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
+ "suggest": {
+ "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests"
},
+ "type": "library",
"autoload": {
- "psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
- }
+ "classmap": [
+ "src/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -2875,111 +3682,52 @@
],
"authors": [
{
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com",
- "homepage": "http://ocramius.github.com/"
+ "name": "Michael Bodnarchuk"
+ },
+ {
+ "name": "Gintautas Miselis"
}
],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "description": "Codeception module for testing web application over HTTP",
+ "homepage": "http://codeception.com/",
"keywords": [
- "constructor",
- "instantiate"
+ "codeception",
+ "functional-testing",
+ "http"
],
- "time": "2019-10-21T16:45:58+00:00"
+ "support": {
+ "issues": "https://github.com/Codeception/module-phpbrowser/issues",
+ "source": "https://github.com/Codeception/module-phpbrowser/tree/1.0.2"
+ },
+ "time": "2020-10-24T15:29:28+00:00"
},
{
- "name": "facebook/webdriver",
- "version": "1.7.1",
+ "name": "codeception/phpunit-wrapper",
+ "version": "9.0.6",
"source": {
"type": "git",
- "url": "https://github.com/php-webdriver/php-webdriver-archive.git",
- "reference": "e43de70f3c7166169d0f14a374505392734160e5"
+ "url": "https://github.com/Codeception/phpunit-wrapper.git",
+ "reference": "b0c06abb3181eedca690170f7ed0fd26a70bfacc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-webdriver/php-webdriver-archive/zipball/e43de70f3c7166169d0f14a374505392734160e5",
- "reference": "e43de70f3c7166169d0f14a374505392734160e5",
+ "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/b0c06abb3181eedca690170f7ed0fd26a70bfacc",
+ "reference": "b0c06abb3181eedca690170f7ed0fd26a70bfacc",
"shasum": ""
},
"require": {
- "ext-curl": "*",
- "ext-json": "*",
- "ext-mbstring": "*",
- "ext-zip": "*",
- "php": "^5.6 || ~7.0",
- "symfony/process": "^2.8 || ^3.1 || ^4.0"
+ "php": ">=7.2",
+ "phpunit/phpunit": "^9.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.0",
- "jakub-onderka/php-parallel-lint": "^0.9.2",
- "php-coveralls/php-coveralls": "^2.0",
- "php-mock/php-mock-phpunit": "^1.1",
- "phpunit/phpunit": "^5.7",
- "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0",
- "squizlabs/php_codesniffer": "^2.6",
- "symfony/var-dumper": "^3.3 || ^4.0"
- },
- "suggest": {
- "ext-SimpleXML": "For Firefox profile creation"
+ "codeception/specify": "*",
+ "consolidation/robo": "^3.0.0-alpha3",
+ "vlucas/phpdotenv": "^3.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-community": "1.5-dev"
- }
- },
"autoload": {
"psr-4": {
- "Facebook\\WebDriver\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache-2.0"
- ],
- "description": "A PHP client for Selenium WebDriver",
- "homepage": "https://github.com/facebook/php-webdriver",
- "keywords": [
- "facebook",
- "php",
- "selenium",
- "webdriver"
- ],
- "abandoned": "php-webdriver/webdriver",
- "time": "2019-06-13T08:02:18+00:00"
- },
- {
- "name": "fzaninotto/faker",
- "version": "v1.9.1",
- "source": {
- "type": "git",
- "url": "https://github.com/fzaninotto/Faker.git",
- "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/fc10d778e4b84d5bd315dad194661e091d307c6f",
- "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f",
- "shasum": ""
- },
- "require": {
- "php": "^5.3.3 || ^7.0"
- },
- "require-dev": {
- "ext-intl": "*",
- "phpunit/phpunit": "^4.8.35 || ^5.7",
- "squizlabs/php_codesniffer": "^2.9.2"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.9-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Faker\\": "src/Faker/"
+ "Codeception\\PHPUnit\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2988,165 +3736,84 @@
],
"authors": [
{
- "name": "François Zaninotto"
- }
- ],
- "description": "Faker is a PHP library that generates fake data for you.",
- "keywords": [
- "data",
- "faker",
- "fixtures"
- ],
- "time": "2019-12-12T13:22:17+00:00"
- },
- {
- "name": "guzzlehttp/guzzle",
- "version": "6.5.2",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/guzzle.git",
- "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
- "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "guzzlehttp/promises": "^1.0",
- "guzzlehttp/psr7": "^1.6.1",
- "php": ">=5.5"
- },
- "require-dev": {
- "ext-curl": "*",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
- "psr/log": "^1.1"
- },
- "suggest": {
- "ext-intl": "Required for Internationalized Domain Name (IDN) support",
- "psr/log": "Required for using the Log middleware"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "6.5-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "GuzzleHttp\\": "src/"
+ "name": "Davert",
+ "email": "davert.php@resend.cc"
},
- "files": [
- "src/functions_include.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
{
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
+ "name": "Naktibalda"
}
],
- "description": "Guzzle is a PHP HTTP client library",
- "homepage": "http://guzzlephp.org/",
- "keywords": [
- "client",
- "curl",
- "framework",
- "http",
- "http client",
- "rest",
- "web service"
- ],
- "time": "2019-12-23T11:57:10+00:00"
+ "description": "PHPUnit classes used by Codeception",
+ "support": {
+ "issues": "https://github.com/Codeception/phpunit-wrapper/issues",
+ "source": "https://github.com/Codeception/phpunit-wrapper/tree/9.0.6"
+ },
+ "time": "2020-12-28T13:59:47+00:00"
},
{
- "name": "guzzlehttp/promises",
- "version": "v1.3.1",
+ "name": "codeception/stub",
+ "version": "3.7.0",
"source": {
"type": "git",
- "url": "https://github.com/guzzle/promises.git",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+ "url": "https://github.com/Codeception/Stub.git",
+ "reference": "468dd5fe659f131fc997f5196aad87512f9b1304"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "url": "https://api.github.com/repos/Codeception/Stub/zipball/468dd5fe659f131fc997f5196aad87512f9b1304",
+ "reference": "468dd5fe659f131fc997f5196aad87512f9b1304",
"shasum": ""
},
"require": {
- "php": ">=5.5.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0"
+ "phpunit/phpunit": "^8.4 | ^9.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4-dev"
- }
- },
"autoload": {
"psr-4": {
- "GuzzleHttp\\Promise\\": "src/"
- },
- "files": [
- "src/functions_include.php"
- ]
+ "Codeception\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- }
- ],
- "description": "Guzzle promises library",
- "keywords": [
- "promise"
- ],
- "time": "2016-12-20T10:07:11+00:00"
+ "description": "Flexible Stub wrapper for PHPUnit's Mock Builder",
+ "support": {
+ "issues": "https://github.com/Codeception/Stub/issues",
+ "source": "https://github.com/Codeception/Stub/tree/3.7.0"
+ },
+ "time": "2020-07-03T15:54:43+00:00"
},
{
- "name": "jean85/pretty-package-versions",
- "version": "1.2",
+ "name": "doctrine/instantiator",
+ "version": "1.4.0",
"source": {
"type": "git",
- "url": "https://github.com/Jean85/pretty-package-versions.git",
- "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48"
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/75c7effcf3f77501d0e0caa75111aff4daa0dd48",
- "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
"shasum": ""
},
"require": {
- "ocramius/package-versions": "^1.2.0",
- "php": "^7.0"
+ "php": "^7.1 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "doctrine/coding-standard": "^8.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
- },
"autoload": {
"psr-4": {
- "Jean85\\": "src/"
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -3155,608 +3822,317 @@
],
"authors": [
{
- "name": "Alessandro Lai",
- "email": "alessandro.lai85@gmail.com"
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
}
],
- "description": "A wrapper for ocramius/package-versions to get pretty versions strings",
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
- "composer",
- "package",
- "release",
- "versions"
+ "constructor",
+ "instantiate"
],
- "time": "2018-06-13T13:22:40+00:00"
- },
- {
- "name": "myclabs/deep-copy",
- "version": "1.9.5",
- "source": {
- "type": "git",
- "url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
- "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "replace": {
- "myclabs/deep-copy": "self.version"
- },
- "require-dev": {
- "doctrine/collections": "^1.0",
- "doctrine/common": "^2.6",
- "phpunit/phpunit": "^7.1"
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
},
- "type": "library",
- "autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
},
- "files": [
- "src/DeepCopy/deep_copy.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Create deep copies (clones) of your objects",
- "keywords": [
- "clone",
- "copy",
- "duplicate",
- "object",
- "object graph"
- ],
- "time": "2020-01-17T21:11:47+00:00"
- },
- {
- "name": "nette/bootstrap",
- "version": "v3.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/nette/bootstrap.git",
- "reference": "b45a1e33b6a44beb307756522396551e5a9ff249"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nette/bootstrap/zipball/b45a1e33b6a44beb307756522396551e5a9ff249",
- "reference": "b45a1e33b6a44beb307756522396551e5a9ff249",
- "shasum": ""
- },
- "require": {
- "nette/di": "^3.0",
- "nette/utils": "^3.0",
- "php": ">=7.1"
- },
- "conflict": {
- "tracy/tracy": "<2.6"
- },
- "require-dev": {
- "latte/latte": "^2.2",
- "nette/application": "^3.0",
- "nette/caching": "^3.0",
- "nette/database": "^3.0",
- "nette/forms": "^3.0",
- "nette/http": "^3.0",
- "nette/mail": "^3.0",
- "nette/robot-loader": "^3.0",
- "nette/safe-stream": "^2.2",
- "nette/security": "^3.0",
- "nette/tester": "^2.0",
- "tracy/tracy": "^2.6"
- },
- "suggest": {
- "nette/robot-loader": "to use Configurator::createRobotLoader()",
- "tracy/tracy": "to use Configurator::enableTracy()"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause",
- "GPL-2.0",
- "GPL-3.0"
- ],
- "authors": [
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
},
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
}
],
- "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.",
- "homepage": "https://nette.org",
- "keywords": [
- "bootstrapping",
- "configurator",
- "nette"
- ],
- "time": "2019-09-30T08:19:38+00:00"
+ "time": "2020-11-10T18:47:58+00:00"
},
{
- "name": "nette/di",
- "version": "v3.0.3",
+ "name": "getgrav/markdowndocs",
+ "version": "2.0.1",
"source": {
"type": "git",
- "url": "https://github.com/nette/di.git",
- "reference": "77d69061cbf8f9cfb7363dd983136f51213d3e41"
+ "url": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator.git",
+ "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/di/zipball/77d69061cbf8f9cfb7363dd983136f51213d3e41",
- "reference": "77d69061cbf8f9cfb7363dd983136f51213d3e41",
+ "url": "https://api.github.com/repos/getgrav/PHP-Markdown-Documentation-Generator/zipball/4a24d1b64a88da17e8f1696dc64969f5ca769064",
+ "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064",
"shasum": ""
},
"require": {
- "ext-tokenizer": "*",
- "nette/neon": "^3.0",
- "nette/php-generator": "^3.3.3",
- "nette/robot-loader": "^3.2",
- "nette/schema": "^1.0",
- "nette/utils": "^3.1",
- "php": ">=7.1"
- },
- "conflict": {
- "nette/bootstrap": "<3.0"
+ "php": ">=5.5.0",
+ "symfony/console": ">=2.6"
},
"require-dev": {
- "nette/tester": "^2.2",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
+ "phpunit/phpunit": "3.7.23"
},
+ "bin": [
+ "bin/phpdoc-md"
+ ],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-0": {
+ "PHPDocsMD": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause",
- "GPL-2.0-only",
- "GPL-3.0-only"
+ "MIT"
],
"authors": [
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
+ "name": "Victor Jonsson",
+ "email": "kontakt@victorjonsson.se"
},
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "name": "Grav CMS",
+ "email": "hello@getgrav.org",
+ "homepage": "https://getgrav.org"
}
],
- "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.",
- "homepage": "https://nette.org",
- "keywords": [
- "compiled",
- "di",
- "dic",
- "factory",
- "ioc",
- "nette",
- "static"
- ],
- "time": "2020-01-20T12:14:54+00:00"
+ "description": "Command line tool for generating markdown-formatted class documentation",
+ "homepage": "https://github.com/victorjonsson/PHP-Markdown-Documentation-Generator",
+ "support": {
+ "source": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator/tree/2.0.1"
+ },
+ "time": "2021-04-20T06:04:42+00:00"
},
{
- "name": "nette/finder",
- "version": "v2.5.2",
+ "name": "guzzlehttp/guzzle",
+ "version": "7.3.0",
"source": {
"type": "git",
- "url": "https://github.com/nette/finder.git",
- "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50"
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "7008573787b430c1c1f650e3722d9bba59967628"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/finder/zipball/4ad2c298eb8c687dd0e74ae84206a4186eeaed50",
- "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628",
+ "reference": "7008573787b430c1c1f650e3722d9bba59967628",
"shasum": ""
},
"require": {
- "nette/utils": "^2.4 || ^3.0",
- "php": ">=7.1"
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.4",
+ "guzzlehttp/psr7": "^1.7 || ^2.0",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0"
},
- "conflict": {
- "nette/nette": "<2.2"
+ "provide": {
+ "psr/http-client-implementation": "1.0"
},
"require-dev": {
- "nette/tester": "^2.0",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
+ "bamarni/composer-bin-plugin": "^1.4.1",
+ "ext-curl": "*",
+ "php-http/client-integration-tests": "^3.0",
+ "phpunit/phpunit": "^8.5.5 || ^9.3.5",
+ "psr/log": "^1.1"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev"
+ "dev-master": "7.3-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause",
- "GPL-2.0",
- "GPL-3.0"
+ "MIT"
],
"authors": [
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
},
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
}
],
- "description": "🔍 Nette Finder: find files and directories with an intuitive API.",
- "homepage": "https://nette.org",
+ "description": "Guzzle is a PHP HTTP client library",
+ "homepage": "http://guzzlephp.org/",
"keywords": [
- "filesystem",
- "glob",
- "iterator",
- "nette"
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
],
- "time": "2020-01-03T20:35:40+00:00"
- },
- {
- "name": "nette/neon",
- "version": "v3.1.2",
- "source": {
- "type": "git",
- "url": "https://github.com/nette/neon.git",
- "reference": "3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nette/neon/zipball/3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e",
- "reference": "3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e",
- "shasum": ""
- },
- "require": {
- "ext-iconv": "*",
- "ext-json": "*",
- "php": ">=7.1"
- },
- "require-dev": {
- "nette/tester": "^2.0",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.3.0"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause",
- "GPL-2.0-only",
- "GPL-3.0-only"
- ],
- "authors": [
+ "funding": [
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
},
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
- }
- ],
- "description": "🍸 Nette NEON: encodes and decodes NEON file format.",
- "homepage": "https://ne-on.org",
- "keywords": [
- "export",
- "import",
- "neon",
- "nette",
- "yaml"
- ],
- "time": "2020-03-04T11:47:04+00:00"
- },
- {
- "name": "nette/php-generator",
- "version": "v3.3.4",
- "source": {
- "type": "git",
- "url": "https://github.com/nette/php-generator.git",
- "reference": "8fe7e699dca7db186f56d75800cb1ec32e39c856"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nette/php-generator/zipball/8fe7e699dca7db186f56d75800cb1ec32e39c856",
- "reference": "8fe7e699dca7db186f56d75800cb1ec32e39c856",
- "shasum": ""
- },
- "require": {
- "nette/utils": "^2.4.2 || ^3.0",
- "php": ">=7.1"
- },
- "require-dev": {
- "nette/tester": "^2.0",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.3-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause",
- "GPL-2.0-only",
- "GPL-3.0-only"
- ],
- "authors": [
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
+ "url": "https://github.com/alexeyshockov",
+ "type": "github"
},
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "url": "https://github.com/gmponos",
+ "type": "github"
}
],
- "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.",
- "homepage": "https://nette.org",
- "keywords": [
- "code",
- "nette",
- "php",
- "scaffolding"
- ],
- "time": "2020-02-09T14:39:09+00:00"
+ "time": "2021-03-23T11:33:13+00:00"
},
{
- "name": "nette/robot-loader",
- "version": "v3.2.3",
+ "name": "guzzlehttp/promises",
+ "version": "1.4.1",
"source": {
"type": "git",
- "url": "https://github.com/nette/robot-loader.git",
- "reference": "726c462e73e739e965ec654a667407074cfe83c0"
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/robot-loader/zipball/726c462e73e739e965ec654a667407074cfe83c0",
- "reference": "726c462e73e739e965ec654a667407074cfe83c0",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
+ "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
"shasum": ""
},
"require": {
- "ext-tokenizer": "*",
- "nette/finder": "^2.5 || ^3.0",
- "nette/utils": "^3.0",
- "php": ">=7.1"
+ "php": ">=5.5"
},
"require-dev": {
- "nette/tester": "^2.0",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
+ "symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.2-dev"
+ "dev-master": "1.4-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause",
- "GPL-2.0-only",
- "GPL-3.0-only"
+ "MIT"
],
"authors": [
{
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
- },
- {
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
}
],
- "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.",
- "homepage": "https://nette.org",
+ "description": "Guzzle promises library",
"keywords": [
- "autoload",
- "class",
- "interface",
- "nette",
- "trait"
+ "promise"
],
- "time": "2020-02-28T13:10:07+00:00"
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/1.4.1"
+ },
+ "time": "2021-03-07T09:25:29+00:00"
},
{
- "name": "nette/schema",
- "version": "v1.0.2",
+ "name": "myclabs/deep-copy",
+ "version": "1.10.2",
"source": {
"type": "git",
- "url": "https://github.com/nette/schema.git",
- "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4"
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/schema/zipball/febf71fb4052c824046f5a33f4f769a6e7fa0cb4",
- "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
"shasum": ""
},
"require": {
- "nette/utils": "^3.1",
- "php": ">=7.1"
+ "php": "^7.1 || ^8.0"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
},
"require-dev": {
- "nette/tester": "^2.2",
- "phpstan/phpstan-nette": "^0.12",
- "tracy/tracy": "^2.3"
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
- "extra": {
- "branch-alias": []
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause",
- "GPL-2.0",
- "GPL-3.0"
- ],
- "authors": [
- {
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
- },
- {
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
- }
+ "MIT"
],
- "description": "📐 Nette Schema: validating data structures against a given Schema.",
- "homepage": "https://nette.org",
+ "description": "Create deep copies (clones) of your objects",
"keywords": [
- "config",
- "nette"
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
],
- "time": "2020-01-06T22:52:48+00:00"
- },
- {
- "name": "nette/utils",
- "version": "v3.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/nette/utils.git",
- "reference": "2c17d16d8887579ae1c0898ff94a3668997fd3eb"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/2c17d16d8887579ae1c0898ff94a3668997fd3eb",
- "reference": "2c17d16d8887579ae1c0898ff94a3668997fd3eb",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "require-dev": {
- "nette/tester": "~2.0",
- "phpstan/phpstan": "^0.12",
- "tracy/tracy": "^2.3"
- },
- "suggest": {
- "ext-gd": "to use Image",
- "ext-iconv": "to use Strings::webalize() and toAscii()",
- "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
- "ext-json": "to use Nette\\Utils\\Json",
- "ext-mbstring": "to use Strings::lower() etc...",
- "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
- "ext-xml": "to use Strings::length() etc. when mbstring is not available"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause",
- "GPL-2.0-only",
- "GPL-3.0-only"
- ],
- "authors": [
- {
- "name": "David Grudl",
- "homepage": "https://davidgrudl.com"
- },
+ "funding": [
{
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
}
],
- "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
- "homepage": "https://nette.org",
- "keywords": [
- "array",
- "core",
- "datetime",
- "images",
- "json",
- "nette",
- "paginator",
- "password",
- "slugify",
- "string",
- "unicode",
- "utf-8",
- "utility",
- "validation"
- ],
- "time": "2020-02-09T14:10:55+00:00"
+ "time": "2020-11-13T09:40:50+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v4.3.0",
+ "version": "v4.13.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc"
+ "reference": "50953a2691a922aa1769461637869a0a2faa3f53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9a9981c347c5c49d6dfe5cf826bb882b824080dc",
- "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53",
+ "reference": "50953a2691a922aa1769461637869a0a2faa3f53",
"shasum": ""
},
"require": {
@@ -3764,8 +4140,8 @@
"php": ">=7.0"
},
"require-dev": {
- "ircmaxell/php-yacc": "0.0.5",
- "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0"
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
},
"bin": [
"bin/php-parse"
@@ -3773,7 +4149,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.3-dev"
+ "dev-master": "4.9-dev"
}
},
"autoload": {
@@ -3795,82 +4171,37 @@
"parser",
"php"
],
- "time": "2019-11-08T13:50:10+00:00"
- },
- {
- "name": "ocramius/package-versions",
- "version": "1.4.2",
- "source": {
- "type": "git",
- "url": "https://github.com/Ocramius/PackageVersions.git",
- "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/44af6f3a2e2e04f2af46bcb302ad9600cba41c7d",
- "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d",
- "shasum": ""
- },
- "require": {
- "composer-plugin-api": "^1.0.0",
- "php": "^7.1.0"
- },
- "require-dev": {
- "composer/composer": "^1.6.3",
- "doctrine/coding-standard": "^5.0.1",
- "ext-zip": "*",
- "infection/infection": "^0.7.1",
- "phpunit/phpunit": "^7.5.17"
- },
- "type": "composer-plugin",
- "extra": {
- "class": "PackageVersions\\Installer",
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "PackageVersions\\": "src/PackageVersions"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com"
- }
- ],
- "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
- "time": "2019-11-15T16:17:10+00:00"
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0"
+ },
+ "time": "2021-09-20T12:20:58+00:00"
},
{
"name": "phar-io/manifest",
- "version": "1.0.3",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
- "phar-io/version": "^2.0",
- "php": "^5.6 || ^7.0"
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
@@ -3900,24 +4231,28 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
- "time": "2018-07-08T19:23:20+00:00"
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+ },
+ "time": "2021-07-20T11:28:43+00:00"
},
{
"name": "phar-io/version",
- "version": "2.0.1",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@@ -3947,32 +4282,33 @@
}
],
"description": "Library for handling version information and constraints",
- "time": "2018-07-08T19:19:57+00:00"
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.1.0"
+ },
+ "time": "2021-02-23T14:00:09+00:00"
},
{
"name": "phpdocumentor/reflection-common",
- "version": "2.0.0",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
- "php": ">=7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "~6"
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.x-dev"
+ "dev-2.x": "2.x-dev"
}
},
"autoload": {
@@ -3999,45 +4335,45 @@
"reflection",
"static analysis"
],
- "time": "2018-08-07T13:53:10+00:00"
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "4.3.4",
+ "version": "5.2.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c"
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c",
- "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
"shasum": ""
},
"require": {
- "php": "^7.0",
- "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
- "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
- "webmozart/assert": "^1.0"
+ "ext-filter": "*",
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.3",
+ "webmozart/assert": "^1.9.1"
},
"require-dev": {
- "doctrine/instantiator": "^1.0.5",
- "mockery/mockery": "^1.0",
- "phpdocumentor/type-resolver": "0.4.*",
- "phpunit/phpunit": "^6.4"
+ "mockery/mockery": "~1.3.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.x-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src/"
- ]
+ "phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -4048,38 +4384,45 @@
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "account@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2019-12-28T18:55:12+00:00"
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+ },
+ "time": "2020-09-03T19:13:55+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "1.0.1",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
+ "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
- "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f",
+ "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
- "ext-tokenizer": "^7.1",
- "mockery/mockery": "~1",
- "phpunit/phpunit": "^7.0"
+ "ext-tokenizer": "*",
+ "psalm/phar": "^4.8"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-1.x": "1.x-dev"
}
},
"autoload": {
@@ -4098,37 +4441,41 @@
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
- "time": "2019-08-22T18:11:29+00:00"
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0"
+ },
+ "time": "2021-09-17T15:28:14+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "v1.10.3",
+ "version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "451c3cd1418cf640de218914901e51b064abb093"
+ "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
- "reference": "451c3cd1418cf640de218914901e51b064abb093",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
+ "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.0.2",
- "php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
- "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
- "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
+ "doctrine/instantiator": "^1.2",
+ "php": "^7.2 || ~8.0, <8.2",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "sebastian/comparator": "^3.0 || ^4.0",
+ "sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
- "phpspec/phpspec": "^2.5 || ^3.2",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+ "phpspec/phpspec": "^6.0 || ^7.0",
+ "phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.10.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
@@ -4161,162 +4508,104 @@
"spy",
"stub"
],
- "time": "2020-03-05T15:02:03+00:00"
- },
- {
- "name": "phpstan/phpdoc-parser",
- "version": "0.3.5",
- "source": {
- "type": "git",
- "url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8c4ef2aefd9788238897b678a985e1d5c8df6db4",
- "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4",
- "shasum": ""
- },
- "require": {
- "php": "~7.1"
- },
- "require-dev": {
- "consistence/coding-standard": "^3.5",
- "jakub-onderka/php-parallel-lint": "^0.9.2",
- "phing/phing": "^2.16.0",
- "phpstan/phpstan": "^0.10",
- "phpunit/phpunit": "^6.3",
- "slevomat/coding-standard": "^4.7.2",
- "squizlabs/php_codesniffer": "^3.3.2",
- "symfony/process": "^3.4 || ^4.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "0.3-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "PHPStan\\PhpDocParser\\": [
- "src/"
- ]
- }
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/1.14.0"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "PHPDoc parser with support for nullable, intersection and generic types",
- "time": "2019-06-07T19:13:52+00:00"
+ "time": "2021-09-10T09:02:12+00:00"
},
{
"name": "phpstan/phpstan",
- "version": "0.11.19",
+ "version": "0.12.99",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "63cc502f6957b7f74efbac444b4cf219dcadffd7"
+ "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/63cc502f6957b7f74efbac444b4cf219dcadffd7",
- "reference": "63cc502f6957b7f74efbac444b4cf219dcadffd7",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4d40f1d759942f523be267a1bab6884f46ca3f7",
+ "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7",
"shasum": ""
},
"require": {
- "composer/xdebug-handler": "^1.3.0",
- "jean85/pretty-package-versions": "^1.0.3",
- "nette/bootstrap": "^2.4 || ^3.0",
- "nette/di": "^2.4.7 || ^3.0",
- "nette/neon": "^2.4.3 || ^3.0",
- "nette/robot-loader": "^3.0.1",
- "nette/schema": "^1.0",
- "nette/utils": "^2.4.5 || ^3.0",
- "nikic/php-parser": "^4.2.3",
- "php": "~7.1",
- "phpstan/phpdoc-parser": "^0.3.5",
- "symfony/console": "~3.2 || ~4.0",
- "symfony/finder": "~3.2 || ~4.0"
+ "php": "^7.1|^8.0"
},
"conflict": {
- "symfony/console": "3.4.16 || 4.1.5"
- },
- "require-dev": {
- "brianium/paratest": "^2.0 || ^3.0",
- "consistence/coding-standard": "^3.5",
- "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
- "ext-intl": "*",
- "ext-mysqli": "*",
- "ext-simplexml": "*",
- "ext-soap": "*",
- "ext-zip": "*",
- "jakub-onderka/php-parallel-lint": "^1.0",
- "localheinz/composer-normalize": "^1.1.0",
- "phing/phing": "^2.16.0",
- "phpstan/phpstan-deprecation-rules": "^0.11",
- "phpstan/phpstan-php-parser": "^0.11",
- "phpstan/phpstan-phpunit": "^0.11",
- "phpstan/phpstan-strict-rules": "^0.11",
- "phpunit/phpunit": "^7.5.14 || ^8.0",
- "slevomat/coding-standard": "^4.7.2",
- "squizlabs/php_codesniffer": "^3.3.2"
+ "phpstan/phpstan-shim": "*"
},
"bin": [
- "bin/phpstan"
+ "phpstan",
+ "phpstan.phar"
],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "0.11-dev"
+ "dev-master": "0.12-dev"
}
},
"autoload": {
- "psr-4": {
- "PHPStan\\": [
- "src/"
- ]
- }
+ "files": [
+ "bootstrap.php"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
- "time": "2019-10-22T20:20:22+00:00"
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "source": "https://github.com/phpstan/phpstan/tree/0.12.99"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/phpstan",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-09-12T20:09:55+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
- "version": "0.11.2",
+ "version": "0.12.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
- "reference": "5685fe48873efc5af1f2cc95d9c1b8ae82c728fe"
+ "reference": "46dbd43c2db973d2876d6653e53f5c2cc3a01fbb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/5685fe48873efc5af1f2cc95d9c1b8ae82c728fe",
- "reference": "5685fe48873efc5af1f2cc95d9c1b8ae82c728fe",
+ "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/46dbd43c2db973d2876d6653e53f5c2cc3a01fbb",
+ "reference": "46dbd43c2db973d2876d6653e53f5c2cc3a01fbb",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.0",
- "php": "~7.1",
- "phpstan/phpstan": "^0.11.8"
+ "php": "^7.1 || ^8.0",
+ "phpstan/phpstan": "^0.12.60"
},
"require-dev": {
- "consistence/coding-standard": "^3.0.1",
- "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
- "jakub-onderka/php-parallel-lint": "^1.0",
- "phing/phing": "^2.16.0",
- "phpstan/phpstan-phpunit": "^0.11",
- "phpunit/phpunit": "^7.0",
- "slevomat/coding-standard": "^4.5.2"
+ "phing/phing": "^2.16.3",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^7.5.20"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
- "dev-master": "0.11-dev"
+ "dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
@@ -4334,44 +4623,52 @@
"MIT"
],
"description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.",
- "time": "2019-05-28T19:54:04+00:00"
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
+ "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/0.12.6"
+ },
+ "time": "2020-12-13T10:20:54+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "6.1.4",
+ "version": "9.2.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
+ "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
- "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218",
+ "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218",
"shasum": ""
},
"require": {
"ext-dom": "*",
+ "ext-libxml": "*",
"ext-xmlwriter": "*",
- "php": "^7.1",
- "phpunit/php-file-iterator": "^2.0",
- "phpunit/php-text-template": "^1.2.1",
- "phpunit/php-token-stream": "^3.0",
- "sebastian/code-unit-reverse-lookup": "^1.0.1",
- "sebastian/environment": "^3.1 || ^4.0",
- "sebastian/version": "^2.0.1",
- "theseer/tokenizer": "^1.1"
+ "nikic/php-parser": "^4.12.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
},
"require-dev": {
- "phpunit/phpunit": "^7.0"
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
- "ext-xdebug": "^2.6.0"
+ "ext-pcov": "*",
+ "ext-xdebug": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.1-dev"
+ "dev-master": "9.2-dev"
}
},
"autoload": {
@@ -4397,32 +4694,42 @@
"testing",
"xunit"
],
- "time": "2018-10-31T16:06:48+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-09-17T05:39:03+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "2.0.2",
+ "version": "3.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "050bedf145a257b1ff02746c31894800e5122946"
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
- "reference": "050bedf145a257b1ff02746c31894800e5122946",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.1"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -4447,29 +4754,266 @@
"filesystem",
"iterator"
],
- "time": "2018-09-13T20:33:42+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:57:25+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
},
{
"name": "phpunit/php-text-template",
- "version": "1.2.1",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.5.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c814a05837f2edb0d1471d6e3f4ab3501ca3899a",
+ "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a",
"shasum": ""
},
- "require": {
- "php": ">=5.3.3"
+ "require": {
+ "doctrine/instantiator": "^1.3.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpspec/prophecy": "^1.12.1",
+ "phpunit/php-code-coverage": "^9.2.7",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.5",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.3",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^2.3.4",
+ "sebastian/version": "^3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*",
+ "phpspec/prophecy-phpunit": "^2.0.1"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*"
},
+ "bin": [
+ "phpunit"
+ ],
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.5-dev"
+ }
+ },
"autoload": {
"classmap": [
"src/"
+ ],
+ "files": [
+ "src/Framework/Assert/Functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -4483,87 +5027,105 @@
"role": "lead"
}
],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
"keywords": [
- "template"
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.10"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/donate.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
],
- "time": "2015-06-21T13:50:34+00:00"
+ "time": "2021-09-25T07:38:51+00:00"
},
{
- "name": "phpunit/php-timer",
- "version": "2.1.2",
+ "name": "psr/http-client",
+ "version": "1.0.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"shasum": ""
},
"require": {
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.0"
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1-dev"
+ "dev-master": "1.0.x-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
}
],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
"keywords": [
- "timer"
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
],
- "time": "2019-06-07T04:22:29+00:00"
+ "support": {
+ "source": "https://github.com/php-fig/http-client/tree/master"
+ },
+ "time": "2020-06-29T06:28:15+00:00"
},
{
- "name": "phpunit/php-token-stream",
- "version": "3.1.1",
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
"shasum": ""
},
"require": {
- "ext-tokenizer": "*",
- "php": "^7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
@@ -4578,73 +5140,48 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
}
],
- "description": "Wrapper around PHP's tokenizer extension.",
- "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
- "keywords": [
- "tokenizer"
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
],
- "time": "2019-09-17T06:23:10+00:00"
+ "time": "2020-09-28T06:08:49+00:00"
},
{
- "name": "phpunit/phpunit",
- "version": "7.5.20",
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
- "reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.1",
- "ext-dom": "*",
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-mbstring": "*",
- "ext-xml": "*",
- "myclabs/deep-copy": "^1.7",
- "phar-io/manifest": "^1.0.2",
- "phar-io/version": "^2.0",
- "php": "^7.1",
- "phpspec/prophecy": "^1.7",
- "phpunit/php-code-coverage": "^6.0.7",
- "phpunit/php-file-iterator": "^2.0.1",
- "phpunit/php-text-template": "^1.2.1",
- "phpunit/php-timer": "^2.1",
- "sebastian/comparator": "^3.0",
- "sebastian/diff": "^3.0",
- "sebastian/environment": "^4.0",
- "sebastian/exporter": "^3.1",
- "sebastian/global-state": "^2.0",
- "sebastian/object-enumerator": "^3.0.3",
- "sebastian/resource-operations": "^2.0",
- "sebastian/version": "^2.0.1"
- },
- "conflict": {
- "phpunit/phpunit-mock-objects": "*"
+ "php": ">=7.3"
},
"require-dev": {
- "ext-pdo": "*"
- },
- "suggest": {
- "ext-soap": "*",
- "ext-xdebug": "*",
- "phpunit/php-invoker": "^2.0"
+ "phpunit/phpunit": "^9.3"
},
- "bin": [
- "phpunit"
- ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "7.5-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
@@ -4663,39 +5200,44 @@
"role": "lead"
}
],
- "description": "The PHP Unit Testing framework.",
- "homepage": "https://phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
],
- "time": "2020-01-08T08:45:45+00:00"
+ "time": "2020-10-26T13:08:54+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.1",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^5.7 || ^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -4715,34 +5257,44 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2017-03-04T06:30:41+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
},
{
"name": "sebastian/comparator",
- "version": "3.0.2",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382",
"shasum": ""
},
"require": {
- "php": "^7.1",
- "sebastian/diff": "^3.0",
- "sebastian/exporter": "^3.1"
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^7.1"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -4755,6 +5307,10 @@
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@@ -4766,10 +5322,6 @@
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
@@ -4779,33 +5331,100 @@
"compare",
"equality"
],
- "time": "2018-07-12T15:12:46+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:49:45+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
},
{
"name": "sebastian/diff",
- "version": "3.0.2",
+ "version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.5 || ^8.0",
- "symfony/process": "^2 || ^3.3 || ^4"
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -4818,13 +5437,13 @@
"BSD-3-Clause"
],
"authors": [
- {
- "name": "Kore Nordmann",
- "email": "mail@kore-nordmann.de"
- },
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
@@ -4835,27 +5454,37 @@
"unidiff",
"unified diff"
],
- "time": "2019-02-04T06:01:07+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:10:38+00:00"
},
{
"name": "sebastian/environment",
- "version": "4.2.3",
+ "version": "5.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
- "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.5"
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
"ext-posix": "*"
@@ -4863,7 +5492,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.2-dev"
+ "dev-master": "5.1-dev"
}
},
"autoload": {
@@ -4888,34 +5517,44 @@
"environment",
"hhvm"
],
- "time": "2019-11-20T08:46:58+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:52:38+00:00"
},
{
"name": "sebastian/exporter",
- "version": "3.1.2",
+ "version": "4.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65",
"shasum": ""
},
"require": {
- "php": "^7.0",
- "sebastian/recursion-context": "^3.0"
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
"ext-mbstring": "*",
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -4955,27 +5594,40 @@
"export",
"exporter"
],
- "time": "2019-09-14T09:02:43+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:24:23+00:00"
},
{
"name": "sebastian/global-state",
- "version": "2.0.0",
+ "version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+ "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49",
+ "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
"ext-uopz": "*"
@@ -4983,7 +5635,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -5006,34 +5658,101 @@
"keywords": [
"global state"
],
- "time": "2017-04-27T15:39:26+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-06-11T13:31:12+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-28T06:42:11+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "3.0.3",
+ "version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
"shasum": ""
},
"require": {
- "php": "^7.0",
- "sebastian/object-reflector": "^1.1.1",
- "sebastian/recursion-context": "^3.0"
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -5053,32 +5772,42 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
- "time": "2017-08-03T12:35:26+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
},
{
"name": "sebastian/object-reflector",
- "version": "1.1.1",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "773f97c67f28de00d397be301821b06708fca0be"
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
- "reference": "773f97c67f28de00d397be301821b06708fca0be",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -5098,32 +5827,42 @@
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
- "time": "2017-03-29T09:07:27+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
},
{
"name": "sebastian/recursion-context",
- "version": "3.0.0",
+ "version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -5136,14 +5875,14 @@
"BSD-3-Clause"
],
"authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
@@ -5151,29 +5890,42 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2017-03-03T06:23:57+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:17:30+00:00"
},
{
"name": "sebastian/resource-operations",
- "version": "2.0.1",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -5193,29 +5945,95 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2018-10-04T04:07:39+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "2.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914",
+ "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/2.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-06-15T12:49:02+00:00"
},
{
"name": "sebastian/version",
- "version": "2.0.1",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
"shasum": ""
},
"require": {
- "php": ">=5.6"
+ "php": ">=7.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -5236,41 +6054,47 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
- "time": "2016-10-03T07:35:21+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
},
{
"name": "symfony/browser-kit",
- "version": "v4.4.5",
+ "version": "v5.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "090ce406505149d6852a7c03b0346dec3b8cf612"
+ "reference": "c1e3f64fcc631c96e2c5843b666db66679ced11c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/090ce406505149d6852a7c03b0346dec3b8cf612",
- "reference": "090ce406505149d6852a7c03b0346dec3b8cf612",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c1e3f64fcc631c96e2c5843b666db66679ced11c",
+ "reference": "c1e3f64fcc631c96e2c5843b666db66679ced11c",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
- "symfony/dom-crawler": "^3.4|^4.0|^5.0"
+ "php": ">=7.2.5",
+ "symfony/dom-crawler": "^4.4|^5.0",
+ "symfony/polyfill-php80": "^1.16"
},
"require-dev": {
- "symfony/css-selector": "^3.4|^4.0|^5.0",
- "symfony/http-client": "^4.3|^5.0",
- "symfony/mime": "^4.3|^5.0",
- "symfony/process": "^3.4|^4.0|^5.0"
+ "symfony/css-selector": "^4.4|^5.0",
+ "symfony/http-client": "^4.4|^5.0",
+ "symfony/mime": "^4.4|^5.0",
+ "symfony/process": "^4.4|^5.0"
},
"suggest": {
"symfony/process": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\BrowserKit\\": ""
@@ -5293,33 +6117,46 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony BrowserKit Component",
+ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"homepage": "https://symfony.com",
- "time": "2020-02-23T10:00:59+00:00"
+ "support": {
+ "source": "https://github.com/symfony/browser-kit/tree/v5.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-21T12:40:44+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v4.4.5",
+ "version": "v5.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22"
+ "reference": "7fb120adc7f600a59027775b224c13a33530dd90"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22",
- "reference": "d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/7fb120adc7f600a59027775b224c13a33530dd90",
+ "reference": "7fb120adc7f600a59027775b224c13a33530dd90",
"shasum": ""
},
"require": {
- "php": "^7.1.3"
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
@@ -5346,45 +6183,126 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony CssSelector Component",
+ "description": "Converts CSS selectors to XPath expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/tree/v5.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-21T12:38:00+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
+ "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
- "time": "2020-02-04T09:01:01+00:00"
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-03-23T23:28:01+00:00"
},
{
"name": "symfony/dom-crawler",
- "version": "v4.4.5",
+ "version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "11dcf08f12f29981bf770f097a5d64d65bce5929"
+ "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/11dcf08f12f29981bf770f097a5d64d65bce5929",
- "reference": "11dcf08f12f29981bf770f097a5d64d65bce5929",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
+ "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
"shasum": ""
},
"require": {
- "php": "^7.1.3",
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-mbstring": "~1.0"
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
},
"conflict": {
"masterminds/html5": "<2.6"
},
"require-dev": {
"masterminds/html5": "^2.6",
- "symfony/css-selector": "^3.4|^4.0|^5.0"
+ "symfony/css-selector": "^4.4|^5.0"
},
"suggest": {
"symfony/css-selector": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\DomCrawler\\": ""
@@ -5407,8 +6325,11 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony DomCrawler Component",
+ "description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -5423,31 +6344,27 @@
"type": "tidelift"
}
],
- "time": "2020-02-29T10:05:28+00:00"
+ "time": "2021-08-29T19:32:13+00:00"
},
{
"name": "symfony/finder",
- "version": "v4.4.5",
+ "version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "ea69c129aed9fdeca781d4b77eb20b62cf5d5357"
+ "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/ea69c129aed9fdeca781d4b77eb20b62cf5d5357",
- "reference": "ea69c129aed9fdeca781d4b77eb20b62cf5d5357",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
+ "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
"shasum": ""
},
"require": {
- "php": "^7.1.3"
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
@@ -5470,8 +6387,11 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Finder Component",
+ "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v5.3.7"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -5486,27 +6406,27 @@
"type": "tidelift"
}
],
- "time": "2020-02-14T07:42:58+00:00"
+ "time": "2021-08-04T21:20:46+00:00"
},
{
"name": "theseer/tokenizer",
- "version": "1.1.3",
+ "version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
- "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
- "php": "^7.0"
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@@ -5526,79 +6446,49 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
- "time": "2019-06-13T22:48:21+00:00"
- },
- {
- "name": "victorjonsson/markdowndocs",
- "version": "dev-master",
- "source": {
- "type": "git",
- "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator.git",
- "reference": "c9fa153b28a79f5da89ec32aa501be92db212aed"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/trilbymedia/PHP-Markdown-Documentation-Generator/zipball/c9fa153b28a79f5da89ec32aa501be92db212aed",
- "reference": "c9fa153b28a79f5da89ec32aa501be92db212aed",
- "shasum": ""
- },
- "require": {
- "php": ">=5.5.0",
- "symfony/console": ">=2.6"
- },
- "require-dev": {
- "phpunit/phpunit": "3.7.23"
- },
- "bin": [
- "bin/phpdoc-md"
- ],
- "type": "library",
- "autoload": {
- "psr-0": {
- "PHPDocsMD": "src/"
- }
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
},
- "license": [
- "MIT"
- ],
- "authors": [
+ "funding": [
{
- "name": "Victor Jonsson",
- "email": "kontakt@victorjonsson.se"
+ "url": "https://github.com/theseer",
+ "type": "github"
}
],
- "description": "Command line tool for generating markdown-formatted class documentation",
- "homepage": "https://github.com/victorjonsson/PHP-Markdown-Documentation-Generator",
- "support": {
- "source": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator/tree/master"
- },
- "time": "2017-09-20T13:29:22+00:00"
+ "time": "2021-07-28T10:34:58+00:00"
},
{
"name": "webmozart/assert",
- "version": "1.7.0",
+ "version": "1.10.0",
"source": {
"type": "git",
- "url": "https://github.com/webmozart/assert.git",
- "reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
- "reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0",
+ "php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
- "vimeo/psalm": "<3.6.0"
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ "phpunit/phpunit": "^8.5.13"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
@@ -5620,29 +6510,30 @@
"check",
"validate"
],
- "time": "2020-02-14T12:15:55+00:00"
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.10.0"
+ },
+ "time": "2021-03-09T10:59:23+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": {
- "willdurand/negotiation": 20,
- "victorjonsson/markdowndocs": 20
- },
+ "stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=7.1.3",
+ "php": "^7.3.6 || ^8.0",
"ext-json": "*",
- "ext-mbstring": "*",
"ext-openssl": "*",
"ext-curl": "*",
"ext-zip": "*",
- "ext-dom": "*"
+ "ext-dom": "*",
+ "ext-libxml": "*"
},
"platform-dev": [],
"platform-overrides": {
- "php": "7.1.3"
+ "php": "7.3.6"
},
- "plugin-api-version": "1.1.0"
+ "plugin-api-version": "2.1.0"
}
diff --git a/index.php b/index.php
index 7be4df1c..091e9a82 100644
--- a/index.php
+++ b/index.php
@@ -3,21 +3,25 @@
/**
* @package Grav.Core
*
- * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav;
\define('GRAV_REQUEST_TIME', microtime(true));
-\define('GRAV_PHP_MIN', '7.1.3');
+\define('GRAV_PHP_MIN', '7.3.6');
if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
die(sprintf('You are running PHP %s, but Grav needs at least PHP %s to run.', $ver, $req));
}
-if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
- die("PHP webserver requires a router to run Grav, please use: php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php");
+if (PHP_SAPI === 'cli-server') {
+ $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
+
+ if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
+ die("PHP webserver requires a router to run Grav, please use: php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php");
+ }
}
// Set timezone to default, falls back to system if php.ini not set
diff --git a/node_modules/ajv/scripts/info b/node_modules/ajv/scripts/info
old mode 100755
new mode 100644
diff --git a/node_modules/ajv/scripts/prepare-tests b/node_modules/ajv/scripts/prepare-tests
old mode 100755
new mode 100644
diff --git a/node_modules/ajv/scripts/publish-built-version b/node_modules/ajv/scripts/publish-built-version
old mode 100755
new mode 100644
diff --git a/node_modules/ajv/scripts/travis-gh-pages b/node_modules/ajv/scripts/travis-gh-pages
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/LICENSE b/node_modules/ecc-jsbn/LICENSE
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/README.md b/node_modules/ecc-jsbn/README.md
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/index.js b/node_modules/ecc-jsbn/index.js
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/lib/LICENSE-jsbn b/node_modules/ecc-jsbn/lib/LICENSE-jsbn
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/lib/ec.js b/node_modules/ecc-jsbn/lib/ec.js
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/lib/sec.js b/node_modules/ecc-jsbn/lib/sec.js
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/package.json b/node_modules/ecc-jsbn/package.json
old mode 100755
new mode 100644
diff --git a/node_modules/ecc-jsbn/test.js b/node_modules/ecc-jsbn/test.js
old mode 100755
new mode 100644
diff --git a/node_modules/in-publish/in-install.js b/node_modules/in-publish/in-install.js
old mode 100755
new mode 100644
diff --git a/node_modules/in-publish/in-publish.js b/node_modules/in-publish/in-publish.js
old mode 100755
new mode 100644
diff --git a/node_modules/in-publish/not-in-install.js b/node_modules/in-publish/not-in-install.js
old mode 100755
new mode 100644
diff --git a/node_modules/in-publish/not-in-publish.js b/node_modules/in-publish/not-in-publish.js
old mode 100755
new mode 100644
diff --git a/node_modules/mkdirp/bin/cmd.js b/node_modules/mkdirp/bin/cmd.js
old mode 100755
new mode 100644
diff --git a/node_modules/nan/tools/1to2.js b/node_modules/nan/tools/1to2.js
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/bin/node-gyp.js b/node_modules/node-gyp/bin/node-gyp.js
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/gyp b/node_modules/node-gyp/gyp/gyp
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/gyp_main.py b/node_modules/node-gyp/gyp/gyp_main.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py b/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/__init__.py b/node_modules/node-gyp/gyp/pylib/gyp/__init__.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/common_test.py b/node_modules/node-gyp/gyp/pylib/gyp/common_test.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py b/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py b/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py b/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/input_test.py b/node_modules/node-gyp/gyp/pylib/gyp/input_test.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py b/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py b/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/samples/samples b/node_modules/node-gyp/gyp/samples/samples
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/setup.py b/node_modules/node-gyp/gyp/setup.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/tools/emacs/run-unit-tests.sh b/node_modules/node-gyp/gyp/tools/emacs/run-unit-tests.sh
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/tools/graphviz.py b/node_modules/node-gyp/gyp/tools/graphviz.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/tools/pretty_gyp.py b/node_modules/node-gyp/gyp/tools/pretty_gyp.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/tools/pretty_sln.py b/node_modules/node-gyp/gyp/tools/pretty_sln.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/gyp/tools/pretty_vcproj.py b/node_modules/node-gyp/gyp/tools/pretty_vcproj.py
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/node_modules/semver/bin/semver b/node_modules/node-gyp/node_modules/semver/bin/semver
old mode 100755
new mode 100644
diff --git a/node_modules/node-gyp/test/docker.sh b/node_modules/node-gyp/test/docker.sh
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/bin/emcc b/node_modules/node-sass/bin/emcc
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/bin/node-sass b/node_modules/node-sass/bin/node-sass
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/bootstrap b/node_modules/node-sass/src/libsass/script/bootstrap
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/branding b/node_modules/node-sass/src/libsass/script/branding
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/ci-build-libsass b/node_modules/node-sass/src/libsass/script/ci-build-libsass
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/ci-build-plugin b/node_modules/node-sass/src/libsass/script/ci-build-plugin
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/ci-install-compiler b/node_modules/node-sass/src/libsass/script/ci-install-compiler
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/ci-install-deps b/node_modules/node-sass/src/libsass/script/ci-install-deps
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/ci-report-coverage b/node_modules/node-sass/src/libsass/script/ci-report-coverage
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/spec b/node_modules/node-sass/src/libsass/script/spec
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/tap-driver b/node_modules/node-sass/src/libsass/script/tap-driver
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/tap-runner b/node_modules/node-sass/src/libsass/script/tap-runner
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/script/test-leaks.pl b/node_modules/node-sass/src/libsass/script/test-leaks.pl
old mode 100755
new mode 100644
diff --git a/node_modules/node-sass/src/libsass/version.sh b/node_modules/node-sass/src/libsass/version.sh
old mode 100755
new mode 100644
diff --git a/node_modules/nopt/bin/nopt.js b/node_modules/nopt/bin/nopt.js
old mode 100755
new mode 100644
diff --git a/node_modules/nopt/examples/my-program.js b/node_modules/nopt/examples/my-program.js
old mode 100755
new mode 100644
diff --git a/node_modules/performance-now/test/scripts/delayed-call.coffee b/node_modules/performance-now/test/scripts/delayed-call.coffee
old mode 100755
new mode 100644
diff --git a/node_modules/performance-now/test/scripts/delayed-require.coffee b/node_modules/performance-now/test/scripts/delayed-require.coffee
old mode 100755
new mode 100644
diff --git a/node_modules/performance-now/test/scripts/difference.coffee b/node_modules/performance-now/test/scripts/difference.coffee
old mode 100755
new mode 100644
diff --git a/node_modules/performance-now/test/scripts/initial-value.coffee b/node_modules/performance-now/test/scripts/initial-value.coffee
old mode 100755
new mode 100644
diff --git a/node_modules/request/index.js b/node_modules/request/index.js
old mode 100755
new mode 100644
diff --git a/node_modules/rimraf/bin.js b/node_modules/rimraf/bin.js
old mode 100755
new mode 100644
diff --git a/node_modules/sass-graph/bin/sassgraph b/node_modules/sass-graph/bin/sassgraph
old mode 100755
new mode 100644
diff --git a/node_modules/semver/bin/semver b/node_modules/semver/bin/semver
old mode 100755
new mode 100644
diff --git a/node_modules/sshpk/bin/sshpk-conv b/node_modules/sshpk/bin/sshpk-conv
old mode 100755
new mode 100644
diff --git a/node_modules/sshpk/bin/sshpk-sign b/node_modules/sshpk/bin/sshpk-sign
old mode 100755
new mode 100644
diff --git a/node_modules/sshpk/bin/sshpk-verify b/node_modules/sshpk/bin/sshpk-verify
old mode 100755
new mode 100644
diff --git a/node_modules/strip-indent/cli.js b/node_modules/strip-indent/cli.js
old mode 100755
new mode 100644
diff --git a/node_modules/uuid/bin/uuid b/node_modules/uuid/bin/uuid
old mode 100755
new mode 100644
diff --git a/node_modules/which/bin/which b/node_modules/which/bin/which
old mode 100755
new mode 100644
diff --git a/node_modules/wrap-ansi/index.js b/node_modules/wrap-ansi/index.js
old mode 100755
new mode 100644
diff --git a/now.json b/now.json
index fe7b94b3..d2e629cf 100644
--- a/now.json
+++ b/now.json
@@ -1,4 +1,4 @@
- {
+{
"version": 2,
"builds": [{ "src": "*.php", "use": "@now/php" }]
}
diff --git a/system/aliases.php b/system/aliases.php
deleted file mode 100644
index 387f3a4c..00000000
--- a/system/aliases.php
+++ /dev/null
@@ -1,5 +0,0 @@
- div > * {
- padding: 5px 15px;
-}
-
-.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
- padding: 5px 8px;
-}
-
-.phpdebugbar div.phpdebugbar-header, .phpdebugbar a.phpdebugbar-restore-btn {
- background-image: url(grav.png);
-}
-
-.phpdebugbar a.phpdebugbar-restore-btn {
- width: 13px;
-}
-
-.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
- background: #3DB9EC;
- color: #fff;
- margin-top: -1px;
- padding-top: 6px;
-}
-
-.phpdebugbar .phpdebugbar-widgets-toolbar {
- border-top: 1px solid #ddd;
- padding-left: 5px;
- padding-right: 2px;
- padding-top: 2px;
- background-color: #fafafa !important;
- width: auto !important;
- left: 0;
- right: 0;
-}
-
-.phpdebugbar .phpdebugbar-widgets-toolbar input {
- background: transparent !important;
-}
-
-.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
-
-}
-
-
-.phpdebugbar input[type=text] {
- padding: 0;
- display: inline;
-}
-
-.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
- font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
- font-size: 12px;
-}
-
-ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
- text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
- top: 0;
-}
-
-.phpdebugbar pre, .phpdebugbar code {
- margin: 0;
- font-size: 14px;
-}
diff --git a/system/assets/debugger/clockwork.css b/system/assets/debugger/clockwork.css
new file mode 100644
index 00000000..e26dab1c
--- /dev/null
+++ b/system/assets/debugger/clockwork.css
@@ -0,0 +1,2 @@
+/** Clockwork Debugger CSS **/
+.clockwork-badge{position:fixed;z-index:10;bottom:0;left:0;padding:2px 4px;background-color:#eee;border:1px solid #ccc;border-bottom:0;border-left:0;display:flex;align-items:center}.clockwork-badge:hover{width:auto}.clockwork-badge:hover:after{content:'Grav Clockwork debugger enabled. Install Clockwork Browser extension (Chrome or Firefox), open your Developer tools and then select the Clockwork tab.'}.clockwork-badge:after{margin-left:10px;font-family:Monaco,Consolas,"Lucida Console",monospace;font-size:12px;line-height:1.5;color:#666}.clockwork-badge i{display:block;float:left;height:22px;width:22px;min-width:22px;background-size:contain;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==)}
diff --git a/system/assets/debugger/clockwork.js b/system/assets/debugger/clockwork.js
new file mode 100644
index 00000000..bb1b69da
--- /dev/null
+++ b/system/assets/debugger/clockwork.js
@@ -0,0 +1,3 @@
+/** Clockwork Debugger JS **/
+document.addEventListener("DOMContentLoaded",function () {
+ var e=document.createElement("div");e.appendChild(document.createElement("i")),e.className="clockwork-badge",document.body.appendChild(e)});
diff --git a/system/assets/debugger/phpdebugbar.css b/system/assets/debugger/phpdebugbar.css
new file mode 100644
index 00000000..93e2d2c3
--- /dev/null
+++ b/system/assets/debugger/phpdebugbar.css
@@ -0,0 +1,70 @@
+div.phpdebugbar {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.phpdebugbar pre {
+ padding: 1rem;
+}
+
+.phpdebugbar div.phpdebugbar-header > div > * {
+ padding: 5px 15px;
+}
+
+.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
+ padding: 5px 8px;
+}
+
+.phpdebugbar div.phpdebugbar-header, .phpdebugbar a.phpdebugbar-restore-btn {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==);
+}
+
+.phpdebugbar a.phpdebugbar-restore-btn {
+ width: 13px;
+}
+
+.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
+ background: #3DB9EC;
+ color: #fff;
+ margin-top: -1px;
+ padding-top: 6px;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar {
+ border-top: 1px solid #ddd;
+ padding-left: 5px;
+ padding-right: 2px;
+ padding-top: 2px;
+ background-color: #fafafa !important;
+ width: auto !important;
+ left: 0;
+ right: 0;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar input {
+ background: transparent !important;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
+
+}
+
+
+.phpdebugbar input[type=text] {
+ padding: 0;
+ display: inline;
+}
+
+.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
+ font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
+ font-size: 12px;
+}
+
+ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
+ text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
+ top: 0;
+}
+
+.phpdebugbar pre, .phpdebugbar code {
+ margin: 0;
+ font-size: 14px;
+}
diff --git a/system/assets/grav.png b/system/assets/grav.png
index 9ae6d97d..67a98b9d 100644
Binary files a/system/assets/grav.png and b/system/assets/grav.png differ
diff --git a/system/assets/jquery/jquery-3.x.min.js b/system/assets/jquery/jquery-3.x.min.js
index a1c07fd8..b0614034 100644
--- a/system/assets/jquery/jquery-3.x.min.js
+++ b/system/assets/jquery/jquery-3.x.min.js
@@ -1,2 +1,2 @@
-/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\n";
diff --git a/system/src/Grav/Common/Assets/Js.php b/system/src/Grav/Common/Assets/Js.php
index cce86d9e..fc2a472f 100644
--- a/system/src/Grav/Common/Assets/Js.php
+++ b/system/src/Grav/Common/Assets/Js.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Assets
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,8 +11,17 @@ namespace Grav\Common\Assets;
use Grav\Common\Utils;
+/**
+ * Class Js
+ * @package Grav\Common\Assets
+ */
class Js extends BaseAsset
{
+ /**
+ * Js constructor.
+ * @param array $elements
+ * @param string|null $key
+ */
public function __construct(array $elements = [], $key = null)
{
$base_options = [
@@ -24,13 +33,16 @@ class Js extends BaseAsset
parent::__construct($merged_attributes, $key);
}
+ /**
+ * @return string
+ */
public function render()
{
if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
- $buffer = $this->gatherLinks( [$this], self::JS_ASSET);
+ $buffer = $this->gatherLinks([$this], self::JS_ASSET);
return '\n";
}
- return '\n";
+ return '\n";
}
}
diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php
index b009585c..948104ab 100644
--- a/system/src/Grav/Common/Assets/Pipeline.php
+++ b/system/src/Grav/Common/Assets/Pipeline.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Assets
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,12 +11,20 @@ namespace Grav\Common\Assets;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Object\PropertyObject;
+use MatthiasMullie\Minify\CSS;
+use MatthiasMullie\Minify\JS;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function array_key_exists;
+/**
+ * Class Pipeline
+ * @package Grav\Common\Assets
+ */
class Pipeline extends PropertyObject
{
use AssetUtilsTrait;
@@ -30,40 +38,42 @@ class Pipeline extends PropertyObject
/** @const Regex to match CSS sourcemap comments */
protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
- /** @const Regex to match CSS import content */
- protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
-
protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
- protected $css_minify;
- protected $css_minify_windows;
- protected $css_rewrite;
-
- protected $js_minify;
- protected $js_minify_windows;
-
- protected $base_url;
+ // Following variables come from the configuration:
+ /** @var bool */
+ protected $css_minify = false;
+ /** @var bool */
+ protected $css_minify_windows = false;
+ /** @var bool */
+ protected $css_rewrite = false;
+ /** @var bool */
+ protected $css_pipeline_include_externals = true;
+ /** @var bool */
+ protected $js_minify = false;
+ /** @var bool */
+ protected $js_minify_windows = false;
+ /** @var bool */
+ protected $js_pipeline_include_externals = true;
+
+ /** @var string */
protected $assets_dir;
+ /** @var string */
protected $assets_url;
+ /** @var string */
protected $timestamp;
+ /** @var array */
protected $attributes;
- protected $query;
+ /** @var string */
+ protected $query = '';
+ /** @var string */
protected $asset;
/**
- * Closure used by the pipeline to fetch assets.
- *
- * Useful when file_get_contents() function is not available in your PHP
- * installation or when you want to apply any kind of preprocessing to
- * your assets before they get pipelined.
- *
- * The closure will receive as the only parameter a string with the path/URL of the asset and
- * it should return the content of the asset file as a string.
- *
- * @var \Closure
+ * Pipeline constructor.
+ * @param array $elements
+ * @param string|null $key
*/
- protected $fetch_command;
-
public function __construct(array $elements = [], ?string $key = null)
{
parent::__construct($elements, $key);
@@ -78,7 +88,14 @@ class Pipeline extends PropertyObject
$uri = Grav::instance()['uri'];
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
- $this->assets_dir = $locator->findResource('asset://') . DS;
+ $this->assets_dir = $locator->findResource('asset://');
+ if (!$this->assets_dir) {
+ // Attempt to create assets folder if it doesn't exist yet.
+ $this->assets_dir = $locator->findResource('asset://', true, true);
+ Folder::mkdir($this->assets_dir);
+ $locator->clearCache();
+ }
+
$this->assets_url = $locator->findResource('asset://', false);
}
@@ -88,7 +105,6 @@ class Pipeline extends PropertyObject
* @param array $assets
* @param string $group
* @param array $attributes
- *
* @return bool|string URL or generated content if available, else false
*/
public function renderCss($assets, $group, $attributes = [])
@@ -110,10 +126,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.css';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
- $buffer = null;
-
- if (file_exists($this->assets_dir . $file)) {
- $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+ $filepath = "{$this->assets_dir}/{$file}";
+ if (file_exists($filepath)) {
+ $buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@@ -125,14 +140,14 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('css')) {
- $minifier = new \MatthiasMullie\Minify\CSS();
+ $minifier = new CSS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
// Write file
if (trim($buffer) !== '') {
- file_put_contents($this->assets_dir . $file, $buffer);
+ file_put_contents($filepath, $buffer);
}
}
@@ -140,7 +155,7 @@ class Pipeline extends PropertyObject
$output = "\n";
} else {
$this->asset = $relative_path;
- $output = 'renderAttributes() . ">\n";
+ $output = 'renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n";
}
return $output;
@@ -152,7 +167,6 @@ class Pipeline extends PropertyObject
* @param array $assets
* @param string $group
* @param array $attributes
- *
* @return bool|string URL or generated content if available, else false
*/
public function renderJs($assets, $group, $attributes = [])
@@ -174,10 +188,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
- $buffer = null;
-
- if (file_exists($this->assets_dir . $file)) {
- $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+ $filepath = "{$this->assets_dir}/{$file}";
+ if (file_exists($filepath)) {
+ $buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@@ -189,14 +202,14 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('js')) {
- $minifier = new \MatthiasMullie\Minify\JS();
+ $minifier = new JS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
// Write file
if (trim($buffer) !== '') {
- file_put_contents($this->assets_dir . $file, $buffer);
+ file_put_contents($filepath, $buffer);
}
}
@@ -204,7 +217,7 @@ class Pipeline extends PropertyObject
$output = '\n";
} else {
$this->asset = $relative_path;
- $output = '\n";
+ $output = '\n";
}
return $output;
@@ -217,8 +230,7 @@ class Pipeline extends PropertyObject
* @param string $file the css source file
* @param string $dir , $local relative path to the css file
* @param bool $local is this a local or remote asset
- *
- * @return mixed
+ * @return string
*/
protected function cssRewrite($file, $dir, $local)
{
@@ -242,18 +254,18 @@ class Pipeline extends PropertyObject
$old_url = ltrim($old_url, '/');
}
- $new_url = ($local ? $this->base_url: '') . $old_url;
+ $new_url = ($local ? $this->base_url : '') . $old_url;
- $fixed = str_replace($matches[2], $new_url, $matches[0]);
-
- return $fixed;
+ return str_replace($matches[2], $new_url, $matches[0]);
}, $file);
return $file;
}
-
-
+ /**
+ * @param string $type
+ * @return bool
+ */
private function shouldMinify($type = 'css')
{
$check = $type . '_minify';
diff --git a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
index 0a1d149d..3f5a690f 100644
--- a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
+++ b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
@@ -3,17 +3,42 @@
/**
* @package Grav\Common\Assets\Traits
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Assets\Traits;
+use Closure;
use Grav\Common\Grav;
use Grav\Common\Utils;
+use function dirname;
+use function in_array;
+use function is_array;
+/**
+ * Trait AssetUtilsTrait
+ * @package Grav\Common\Assets\Traits
+ */
trait AssetUtilsTrait
{
+ /**
+ * @var Closure|null
+ *
+ * Closure used by the pipeline to fetch assets.
+ *
+ * Useful when file_get_contents() function is not available in your PHP
+ * installation or when you want to apply any kind of preprocessing to
+ * your assets before they get pipelined.
+ *
+ * The closure will receive as the only parameter a string with the path/URL of the asset and
+ * it should return the content of the asset file as a string.
+ */
+ protected $fetch_command;
+
+ /** @var string */
+ protected $base_url;
+
/**
* Determine whether a link is local or remote.
* Understands both "http://" and "https://" as well as protocol agnostic links "//"
@@ -38,14 +63,11 @@ trait AssetUtilsTrait
*
* @param array $assets
* @param bool $css
- *
* @return string
*/
protected function gatherLinks(array $assets, $css = true)
{
$buffer = '';
-
-
foreach ($assets as $id => $asset) {
$local = true;
@@ -57,7 +79,7 @@ trait AssetUtilsTrait
if (0 === strpos($link, '//')) {
$link = 'http:' . $link;
}
- $relative_dir = \dirname($relative_path);
+ $relative_dir = dirname($relative_path);
} else {
// Fix to remove relative dir if grav is in one
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
@@ -65,11 +87,12 @@ trait AssetUtilsTrait
$relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
}
- $relative_dir = \dirname($relative_path);
- $link = ROOT_DIR . $relative_path;
+ $relative_dir = dirname($relative_path);
+ $link = GRAV_ROOT . '/' . $relative_path;
}
- $file = ($this->fetch_command instanceof \Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
+ // TODO: looks like this is not being used.
+ $file = $this->fetch_command instanceof Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
// No file found, skip it...
if ($file === false) {
@@ -102,14 +125,15 @@ trait AssetUtilsTrait
* Moves @import statements to the top of the file per the CSS specification
*
* @param string $file the file containing the combined CSS files
- *
* @return string the modified file with any @imports at the top of the file
*/
protected function moveImports($file)
{
+ $regex = '{@import.*?["\']([^"\']+)["\'].*?;}';
+
$imports = [];
- $file = (string)preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) use (&$imports) {
+ $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) {
$imports[] = $matches[0];
return '';
@@ -130,14 +154,18 @@ trait AssetUtilsTrait
$no_key = ['loading'];
foreach ($this->attributes as $key => $value) {
+ if ($value === null) {
+ continue;
+ }
+
if (is_numeric($key)) {
$key = $value;
}
- if (\is_array($value)) {
+ if (is_array($value)) {
$value = implode(' ', $value);
}
- if (\in_array($key, $no_key, true)) {
+ if (in_array($key, $no_key, true)) {
$element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
} else {
$element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
@@ -152,7 +180,7 @@ trait AssetUtilsTrait
/**
* Render Querystring
*
- * @param string $asset
+ * @param string|null $asset
* @return string
*/
protected function renderQueryString($asset = null)
@@ -170,7 +198,7 @@ trait AssetUtilsTrait
}
if ($this->timestamp) {
- if (Utils::contains($asset, '?') || $querystring) {
+ if ($querystring || Utils::contains($asset, '?')) {
$querystring .= '&' . $this->timestamp;
} else {
$querystring .= '?' . $this->timestamp;
diff --git a/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
index 7f91c6da..36b21522 100644
--- a/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
+++ b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
@@ -3,17 +3,23 @@
/**
* @package Grav\Common\Assets\Traits
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Assets\Traits;
use Grav\Common\Assets;
+use function count;
+use function is_array;
+use function is_int;
+/**
+ * Trait LegacyAssetsTrait
+ * @package Grav\Common\Assets\Traits
+ */
trait LegacyAssetsTrait
{
-
/**
* @param array $args
* @param string $type
@@ -39,12 +45,12 @@ trait LegacyAssetsTrait
}
switch ($type) {
- case(Assets::JS_TYPE):
+ case (Assets::JS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
- case(Assets::INLINE_JS_TYPE):
+ case (Assets::INLINE_JS_TYPE):
$defaults = ['priority' => null, 'group' => null, 'attributes' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
@@ -61,13 +67,13 @@ trait LegacyAssetsTrait
break;
- case(Assets::INLINE_CSS_TYPE):
+ case (Assets::INLINE_CSS_TYPE):
$defaults = ['priority' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
default:
- case(Assets::CSS_TYPE):
+ case (Assets::CSS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
}
@@ -75,6 +81,11 @@ trait LegacyAssetsTrait
return $arguments;
}
+ /**
+ * @param array $args
+ * @param array $defaults
+ * @return array
+ */
protected function createArgumentsFromLegacy(array $args, array $defaults)
{
// Remove arguments with old default values.
@@ -97,8 +108,7 @@ trait LegacyAssetsTrait
* @param int $priority
* @param bool $pipeline
* @param string $group name of the group
- *
- * @return \Grav\Common\Assets
+ * @return Assets
* @deprecated Please use dynamic method with ['loading' => 'async'].
*/
public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head')
@@ -115,8 +125,7 @@ trait LegacyAssetsTrait
* @param int $priority
* @param bool $pipeline
* @param string $group name of the group
- *
- * @return \Grav\Common\Assets
+ * @return Assets
* @deprecated Please use dynamic method with ['loading' => 'defer'].
*/
public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head')
@@ -125,5 +134,4 @@ trait LegacyAssetsTrait
return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
}
-
}
diff --git a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
index 84b83e30..15dc00e3 100644
--- a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
+++ b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
@@ -3,21 +3,29 @@
/**
* @package Grav\Common\Assets\Traits
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Assets\Traits;
+use FilesystemIterator;
use Grav\Common\Grav;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+use function strlen;
+/**
+ * Trait TestingAssetsTrait
+ * @package Grav\Common\Assets\Traits
+ */
trait TestingAssetsTrait
{
/**
* Determines if an asset exists as a collection, CSS or JS reference
*
* @param string $asset
- *
* @return bool
*/
public function exists($asset)
@@ -39,7 +47,6 @@ trait TestingAssetsTrait
* Set the array of collections explicitly
*
* @param array $collections
- *
* @return $this
*/
public function setCollection($collections)
@@ -54,7 +61,7 @@ trait TestingAssetsTrait
* If a $key is provided, it will try to return only that asset
* else it will return null
*
- * @param null|string $key the asset key
+ * @param string|null $key the asset key
* @return array
*/
public function getCss($key = null)
@@ -73,7 +80,7 @@ trait TestingAssetsTrait
* If a $key is provided, it will try to return only that asset
* else it will return null
*
- * @param null|string $key the asset key
+ * @param string|null $key the asset key
* @return array
*/
public function getJs($key = null)
@@ -91,7 +98,6 @@ trait TestingAssetsTrait
* Set the whole array of CSS assets
*
* @param array $css
- *
* @return $this
*/
public function setCss($css)
@@ -105,7 +111,6 @@ trait TestingAssetsTrait
* Set the whole array of JS assets
*
* @param array $js
- *
* @return $this
*/
public function setJs($js)
@@ -119,7 +124,6 @@ trait TestingAssetsTrait
* Removes an item from the CSS array if set
*
* @param string $key The asset key
- *
* @return $this
*/
public function removeCss($key)
@@ -136,7 +140,6 @@ trait TestingAssetsTrait
* Removes an item from the JS array if set
*
* @param string $key The asset key
- *
* @return $this
*/
public function removeJs($key)
@@ -153,7 +156,6 @@ trait TestingAssetsTrait
* Sets the state of CSS Pipeline
*
* @param bool $value
- *
* @return $this
*/
public function setCssPipeline($value)
@@ -167,7 +169,6 @@ trait TestingAssetsTrait
* Sets the state of JS Pipeline
*
* @param bool $value
- *
* @return $this
*/
public function setJsPipeline($value)
@@ -188,6 +189,7 @@ trait TestingAssetsTrait
$this->resetJs();
$this->setCssPipeline(false);
$this->setJsPipeline(false);
+ $this->order = [];
return $this;
}
@@ -230,7 +232,7 @@ trait TestingAssetsTrait
* Get the timestamp for assets
*
* @param bool $include_join
- * @return string
+ * @return string|null
*/
public function getTimestamp($include_join = true)
{
@@ -246,12 +248,11 @@ trait TestingAssetsTrait
*
* @param string $directory Relative to the Grav root path, or a stream identifier
* @param string $pattern (regex)
- *
* @return $this
*/
public function addDir($directory, $pattern = self::DEFAULT_REGEX)
{
- $root_dir = rtrim(ROOT_DIR, '/');
+ $root_dir = GRAV_ROOT;
// Check if $directory is a stream.
if (strpos($directory, '://')) {
@@ -296,7 +297,6 @@ trait TestingAssetsTrait
* Add all JavaScript assets within $directory
*
* @param string $directory Relative to the Grav root path, or a stream identifier
- *
* @return $this
*/
public function addDirJs($directory)
@@ -308,7 +308,6 @@ trait TestingAssetsTrait
* Add all CSS assets within $directory
*
* @param string $directory Relative to the Grav root path, or a stream identifier
- *
* @return $this
*/
public function addDirCss($directory)
@@ -321,15 +320,16 @@ trait TestingAssetsTrait
*
* @param string $directory
* @param string $pattern (regex)
- * @param string $ltrim Will be trimmed from the left of the file path
- *
+ * @param string|null $ltrim Will be trimmed from the left of the file path
* @return array
*/
protected function rglob($directory, $pattern, $ltrim = null)
{
- $iterator = new \RegexIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory,
- \FilesystemIterator::SKIP_DOTS)), $pattern);
- $offset = \strlen($ltrim);
+ $iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
+ $directory,
+ FilesystemIterator::SKIP_DOTS
+ )), $pattern);
+ $offset = strlen($ltrim);
$files = [];
foreach ($iterator as $file) {
@@ -338,6 +338,4 @@ trait TestingAssetsTrait
return $files;
}
-
-
}
diff --git a/system/src/Grav/Common/Backup/Backups.php b/system/src/Grav/Common/Backup/Backups.php
index 9582b9e6..b70def96 100644
--- a/system/src/Grav/Common/Backup/Backups.php
+++ b/system/src/Grav/Common/Backup/Backups.php
@@ -3,12 +3,16 @@
/**
* @package Grav\Common\Backup
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Backup;
+use DateTime;
+use Exception;
+use FilesystemIterator;
+use GlobIterator;
use Grav\Common\Filesystem\Archiver;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Inflector;
@@ -17,81 +21,126 @@ use Grav\Common\Scheduler\Scheduler;
use Grav\Common\Utils;
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;
-use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\File\JsonFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use SplFileInfo;
+use stdClass;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use function count;
+/**
+ * Class Backups
+ * @package Grav\Common\Backup
+ */
class Backups
{
protected const BACKUP_FILENAME_REGEXZ = "#(.*)--(\d*).zip#";
protected const BACKUP_DATE_FORMAT = 'YmdHis';
+ /** @var string */
protected static $backup_dir;
- protected static $backups = null;
+ /** @var array|null */
+ protected static $backups;
+ /**
+ * @return void
+ */
public function init()
{
+ $grav = Grav::instance();
+
/** @var EventDispatcher $dispatcher */
- $dispatcher = Grav::instance()['events'];
+ $dispatcher = $grav['events'];
$dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
- Grav::instance()->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
+
+ $grav->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
}
+ /**
+ * @return void
+ */
public function setup()
{
if (null === static::$backup_dir) {
- static::$backup_dir = Grav::instance()['locator']->findResource('backup://', true, true);
+ $grav = Grav::instance();
+ static::$backup_dir = $grav['locator']->findResource('backup://', true, true);
Folder::create(static::$backup_dir);
}
}
+ /**
+ * @param Event $event
+ * @return void
+ */
public function onSchedulerInitialized(Event $event)
{
+ $grav = Grav::instance();
+
/** @var Scheduler $scheduler */
$scheduler = $event['scheduler'];
/** @var Inflector $inflector */
- $inflector = Grav::instance()['inflector'];
+ $inflector = $grav['inflector'];
foreach (static::getBackupProfiles() as $id => $profile) {
$at = $profile['schedule_at'];
$name = $inflector::hyphenize($profile['name']);
$logs = 'logs/backup-' . $name . '.out';
/** @var Job $job */
- $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name );
+ $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name);
$job->at($at);
$job->output($logs);
$job->backlink('/tools/backups');
}
}
+ /**
+ * @param string $backup
+ * @param string $base_url
+ * @return string
+ */
public function getBackupDownloadUrl($backup, $base_url)
{
$param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
- $download = urlencode(base64_encode($backup));
- $url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim($base_url,
- '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
+ $download = urlencode(base64_encode(basename($backup)));
+ $url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim(
+ $base_url,
+ '/'
+ ) . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
return $url;
}
+ /**
+ * @return array
+ */
public static function getBackupProfiles()
{
return Grav::instance()['config']->get('backups.profiles');
}
+ /**
+ * @return array
+ */
public static function getPurgeConfig()
{
return Grav::instance()['config']->get('backups.purge');
}
+ /**
+ * @return array
+ */
public function getBackupNames()
{
return array_column(static::getBackupProfiles(), 'name');
}
+ /**
+ * @return float|int
+ */
public static function getTotalBackupsSize()
{
$backups = static::getAvailableBackups();
@@ -100,24 +149,29 @@ class Backups
return $size ?? 0;
}
+ /**
+ * @param bool $force
+ * @return array
+ */
public static function getAvailableBackups($force = false)
{
if ($force || null === static::$backups) {
static::$backups = [];
- $backups_itr = new \GlobIterator(static::$backup_dir . '/*.zip', \FilesystemIterator::KEY_AS_FILENAME);
- $inflector = Grav::instance()['inflector'];
+
+ $grav = Grav::instance();
+ $backups_itr = new GlobIterator(static::$backup_dir . '/*.zip', FilesystemIterator::KEY_AS_FILENAME);
+ $inflector = $grav['inflector'];
$long_date_format = DATE_RFC2822;
/**
* @var string $name
- * @var \SplFileInfo $file
+ * @var SplFileInfo $file
*/
foreach ($backups_itr as $name => $file) {
-
if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) {
- $date = \DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]);
+ $date = DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]);
$timestamp = $date->getTimestamp();
- $backup = new \stdClass();
+ $backup = new stdClass();
$backup->title = $inflector->titleize($matches[1]);
$backup->time = $date;
$backup->date = $date->format($long_date_format);
@@ -137,28 +191,29 @@ class Backups
/**
* Backup
*
- * @param int $id
+ * @param int $id
* @param callable|null $status
- *
- * @return null|string
+ * @return string|null
*/
public static function backup($id = 0, callable $status = null)
{
+ $grav = Grav::instance();
+
$profiles = static::getBackupProfiles();
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
+ $locator = $grav['locator'];
if (isset($profiles[$id])) {
$backup = (object) $profiles[$id];
} else {
- throw new \RuntimeException('No backups defined...');
+ throw new RuntimeException('No backups defined...');
}
- $name = Grav::instance()['inflector']->underscorize($backup->name);
+ $name = $grav['inflector']->underscorize($backup->name);
$date = date(static::BACKUP_DATE_FORMAT, time());
$filename = trim($name, '_') . '--' . $date . '.zip';
$destination = static::$backup_dir . DS . $filename;
- $max_execution_time = ini_set('max_execution_time', 600);
+ $max_execution_time = ini_set('max_execution_time', '600');
$backup_root = $backup->root;
if ($locator->isStream($backup_root)) {
@@ -167,8 +222,8 @@ class Backups
$backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
}
- if (!file_exists($backup_root)) {
- throw new \RuntimeException("Backup location: {$backup_root} does not exist...");
+ if (!$backup_root || !file_exists($backup_root)) {
+ throw new RuntimeException("Backup location: {$backup_root} does not exist...");
}
$options = [
@@ -176,7 +231,6 @@ class Backups
'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),
];
- /** @var Archiver $archiver */
$archiver = Archiver::create('zip');
$archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status);
@@ -195,16 +249,16 @@ class Backups
}
// Log the backup
- Grav::instance()['log']->notice('Backup Created: ' . $destination);
+ $grav['log']->notice('Backup Created: ' . $destination);
// Fire Finished event
- Grav::instance()->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
+ $grav->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
// Purge anything required
static::purge();
// Log
- $log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true));
+ $log = JsonFile::instance($locator->findResource("log://backup.log", true, true));
$log->content([
'time' => time(),
'location' => $destination
@@ -214,26 +268,29 @@ class Backups
return $destination;
}
+ /**
+ * @return void
+ * @throws Exception
+ */
public static function purge()
{
$purge_config = static::getPurgeConfig();
$trigger = $purge_config['trigger'];
$backups = static::getAvailableBackups(true);
- switch ($trigger)
- {
+ switch ($trigger) {
case 'number':
$backups_count = count($backups);
if ($backups_count > $purge_config['max_backups_count']) {
$last = end($backups);
- unlink ($last->path);
+ unlink($last->path);
static::purge();
}
break;
case 'time':
$last = end($backups);
- $now = new \DateTime();
+ $now = new DateTime();
$interval = $now->diff($last->time);
if ($interval->days > $purge_config['max_backups_time']) {
unlink($last->path);
@@ -253,9 +310,14 @@ class Backups
}
}
+ /**
+ * @param string $exclude
+ * @return array
+ */
protected static function convertExclude($exclude)
{
$lines = preg_split("/[\s,]+/", $exclude);
- return array_map('trim', $lines, array_fill(0, \count($lines), '/'));
+
+ return array_map('trim', $lines, array_fill(0, count($lines), '/'));
}
}
diff --git a/system/src/Grav/Common/Browser.php b/system/src/Grav/Common/Browser.php
index 8d0d6878..e4a6513e 100644
--- a/system/src/Grav/Common/Browser.php
+++ b/system/src/Grav/Common/Browser.php
@@ -3,12 +3,13 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use InvalidArgumentException;
use function donatj\UserAgent\parse_user_agent;
/**
@@ -16,6 +17,7 @@ use function donatj\UserAgent\parse_user_agent;
*/
class Browser
{
+ /** @var string[] */
protected $useragent = [];
/**
@@ -25,7 +27,7 @@ class Browser
{
try {
$this->useragent = parse_user_agent();
- } catch (\InvalidArgumentException $e) {
+ } catch (InvalidArgumentException $e) {
$this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
}
}
@@ -110,7 +112,7 @@ class Browser
/**
* Get the current major version identifier
*
- * @return string the browser major version identifier
+ * @return int the browser major version identifier
*/
public function getVersion()
{
@@ -137,7 +139,7 @@ class Browser
return true;
}
-
+
/**
* Determine if “Do Not Track” is set by browser
* @see https://www.w3.org/TR/tracking-dnt/
diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php
index 94eae595..a5035183 100644
--- a/system/src/Grav/Common/Cache.php
+++ b/system/src/Grav/Common/Cache.php
@@ -3,19 +3,27 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use DirectoryIterator;
use \Doctrine\Common\Cache as DoctrineCache;
+use Exception;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Scheduler\Scheduler;
+use LogicException;
use Psr\SimpleCache\CacheInterface;
use RocketTheme\Toolbox\Event\Event;
-use RocketTheme\Toolbox\Event\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use function dirname;
+use function extension_loaded;
+use function function_exists;
+use function in_array;
+use function is_array;
/**
* The GravCache object is used throughout Grav to store and retrieve cached data.
@@ -29,42 +37,41 @@ use RocketTheme\Toolbox\Event\EventDispatcher;
*/
class Cache extends Getters
{
- /**
- * @var string Cache key.
- */
+ /** @var string Cache key. */
protected $key;
+ /** @var int */
protected $lifetime;
+
+ /** @var int */
protected $now;
/** @var Config $config */
protected $config;
- /**
- * @var DoctrineCache\CacheProvider
- */
+ /** @var DoctrineCache\CacheProvider */
protected $driver;
- /**
- * @var CacheInterface
- */
+ /** @var CacheInterface */
protected $simpleCache;
+ /** @var string */
protected $driver_name;
+ /** @var string */
protected $driver_setting;
- /**
- * @var bool
- */
+ /** @var bool */
protected $enabled;
+ /** @var string */
protected $cache_dir;
protected static $standard_remove = [
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
+ 'cache://clockwork/',
'cache://validated-',
'cache://images',
'asset://',
@@ -74,6 +81,7 @@ class Cache extends Getters
'cache://twig/',
'cache://doctrine/',
'cache://compiled/',
+ 'cache://clockwork/',
'cache://validated-',
'asset://',
];
@@ -115,12 +123,10 @@ class Cache extends Getters
* Initialization that sets a base key and the driver based on configuration settings
*
* @param Grav $grav
- *
* @return void
*/
public function init(Grav $grav)
{
- /** @var Config $config */
$this->config = $grav['config'];
$this->now = time();
@@ -135,7 +141,7 @@ class Cache extends Getters
$uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
// Cache key allows us to invalidate all cache on configuration changes.
- $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
+ $this->key = ($prefix ?: 'g') . '-' . $uniqueness;
$this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
$this->driver_setting = $this->config->get('system.cache.driver');
$this->driver = $this->getCacheDriver();
@@ -174,7 +180,7 @@ class Cache extends Getters
$current = basename($this->cache_dir);
$count = 0;
- foreach (new \DirectoryIterator($cache_dir) as $file) {
+ foreach (new DirectoryIterator($cache_dir) as $file) {
$dir = $file->getBasename();
if ($dir === $current || $file->isDot() || $file->isFile()) {
continue;
@@ -191,6 +197,7 @@ class Cache extends Getters
* Public accessor to set the enabled state of the cache
*
* @param bool|int $enabled
+ * @return void
*/
public function setEnabled($enabled)
{
@@ -260,24 +267,28 @@ class Cache extends Getters
case 'memcache':
if (extension_loaded('memcache')) {
$memcache = new \Memcache();
- $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
- $this->config->get('system.cache.memcache.port', 11211));
+ $memcache->connect(
+ $this->config->get('system.cache.memcache.server', 'localhost'),
+ $this->config->get('system.cache.memcache.port', 11211)
+ );
$driver = new DoctrineCache\MemcacheCache();
$driver->setMemcache($memcache);
} else {
- throw new \LogicException('Memcache PHP extension has not been installed');
+ throw new LogicException('Memcache PHP extension has not been installed');
}
break;
case 'memcached':
if (extension_loaded('memcached')) {
$memcached = new \Memcached();
- $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
- $this->config->get('system.cache.memcached.port', 11211));
+ $memcached->addServer(
+ $this->config->get('system.cache.memcached.server', 'localhost'),
+ $this->config->get('system.cache.memcached.port', 11211)
+ );
$driver = new DoctrineCache\MemcachedCache();
$driver->setMemcached($memcached);
} else {
- throw new \LogicException('Memcached PHP extension has not been installed');
+ throw new LogicException('Memcached PHP extension has not been installed');
}
break;
@@ -286,12 +297,15 @@ class Cache extends Getters
$redis = new \Redis();
$socket = $this->config->get('system.cache.redis.socket', false);
$password = $this->config->get('system.cache.redis.password', false);
+ $databaseId = $this->config->get('system.cache.redis.database', 0);
if ($socket) {
$redis->connect($socket);
} else {
- $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
- $this->config->get('system.cache.redis.port', 6379));
+ $redis->connect(
+ $this->config->get('system.cache.redis.server', 'localhost'),
+ $this->config->get('system.cache.redis.port', 6379)
+ );
}
// Authenticate with password if set
@@ -299,10 +313,15 @@ class Cache extends Getters
throw new \RedisException('Redis authentication failed');
}
+ // Select alternate ( !=0 ) database ID if set
+ if ($databaseId && !$redis->select($databaseId)) {
+ throw new \RedisException('Could not select alternate Redis database ID');
+ }
+
$driver = new DoctrineCache\RedisCache();
$driver->setRedis($redis);
} else {
- throw new \LogicException('Redis PHP extension has not been installed');
+ throw new LogicException('Redis PHP extension has not been installed');
}
break;
@@ -318,8 +337,7 @@ class Cache extends Getters
* Gets a cached entry if it exists based on an id. If it does not exist, it returns false
*
* @param string $id the id of the cached entry
- *
- * @return object|bool returns the cached entry, can be any type, or false if doesn't exist
+ * @return mixed|bool returns the cached entry, can be any type, or false if doesn't exist
*/
public function fetch($id)
{
@@ -334,8 +352,8 @@ class Cache extends Getters
* Stores a new cached entry.
*
* @param string $id the id of the cached entry
- * @param array|object $data the data for the cached entry to store
- * @param int $lifetime the lifetime to store the entry in seconds
+ * @param array|object|int $data the data for the cached entry to store
+ * @param int|null $lifetime the lifetime to store the entry in seconds
*/
public function save($id, $data, $lifetime = null)
{
@@ -393,6 +411,8 @@ class Cache extends Getters
/**
* Getter method to get the cache key
+ *
+ * @return string
*/
public function getKey()
{
@@ -401,6 +421,9 @@ class Cache extends Getters
/**
* Setter method to set key (Advanced)
+ *
+ * @param string $key
+ * @return void
*/
public function setKey($key)
{
@@ -412,7 +435,6 @@ class Cache extends Getters
* Helper method to clear all Grav caches
*
* @param string $remove standard|all|assets-only|images-only|cache-only
- *
* @return array
*/
public static function clearCache($remove = 'standard')
@@ -446,7 +468,6 @@ class Cache extends Getters
} else {
$remove_paths = self::$standard_remove_no_images;
}
-
}
// Delete entries in the doctrine cache if required
@@ -459,11 +480,12 @@ class Cache extends Getters
Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
foreach ($remove_paths as $stream) {
-
// Convert stream to a real path
try {
$path = $locator->findResource($stream, true, true);
- if($path === false) continue;
+ if ($path === false) {
+ continue;
+ }
$anything = false;
$files = glob($path . '/*');
@@ -477,7 +499,7 @@ class Cache extends Getters
$anything = true;
}
} elseif (is_dir($file)) {
- if (Folder::delete($file)) {
+ if (Folder::delete($file, false)) {
$anything = true;
}
}
@@ -487,7 +509,7 @@ class Cache extends Getters
if ($anything) {
$output[] = 'Cleared: ' . $path . '/*';
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// stream not found or another error while deleting files.
$output[] = 'ERROR: ' . $e->getMessage();
}
@@ -510,9 +532,14 @@ class Cache extends Getters
@opcache_reset();
}
+ Grav::instance()->fireEvent('onAfterCacheClear', new Event(['remove' => $remove, 'output' => &$output]));
+
return $output;
}
+ /**
+ * @return void
+ */
public static function invalidateCache()
{
$user_config = USER_DIR . 'config/system.yaml';
@@ -528,13 +555,13 @@ class Cache extends Getters
if (function_exists('opcache_reset')) {
@opcache_reset();
}
-
}
/**
* Set the cache lifetime programmatically
*
* @param int $future timestamp
+ * @return void
*/
public function setLifetime($future)
{
@@ -552,7 +579,7 @@ class Cache extends Getters
/**
* Retrieve the cache lifetime (in seconds)
*
- * @return mixed
+ * @return int
*/
public function getLifetime()
{
@@ -566,7 +593,7 @@ class Cache extends Getters
/**
* Returns the current driver name
*
- * @return mixed
+ * @return string
*/
public function getDriverName()
{
@@ -576,7 +603,7 @@ class Cache extends Getters
/**
* Returns the current driver setting
*
- * @return mixed
+ * @return string
*/
public function getDriverSetting()
{
@@ -591,29 +618,35 @@ class Cache extends Getters
*/
public function isVolatileDriver($setting)
{
- if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
- return true;
- }
-
- return false;
+ return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true);
}
/**
* Static function to call as a scheduled Job to purge old Doctrine files
+ *
+ * @param bool $echo
+ *
+ * @return string|void
*/
- public static function purgeJob()
+ public static function purgeJob($echo = false)
{
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
$deleted_folders = $cache->purgeOldCache();
+ $msg = 'Purged ' . $deleted_folders . ' old cache folders...';
- echo 'Purged ' . $deleted_folders . ' old cache folders...';
+ if ($echo) {
+ echo $msg;
+ } else {
+ return $msg;
+ }
}
/**
* Static function to call as a scheduled Job to clear Grav cache
*
* @param string $type
+ * @return void
*/
public static function clearJob($type)
{
@@ -623,6 +656,10 @@ class Cache extends Getters
echo strip_tags(implode("\n", $result));
}
+ /**
+ * @param Event $event
+ * @return void
+ */
public function onSchedulerInitialized(Event $event)
{
/** @var Scheduler $scheduler */
@@ -634,7 +671,7 @@ class Cache extends Getters
$name = 'cache-purge';
$logs = 'logs/' . $name . '.out';
- $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [], $name );
+ $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [true], $name);
$job->at($at);
$job->output($logs);
$job->backlink('/config/system#caching');
@@ -645,12 +682,9 @@ class Cache extends Getters
$name = 'cache-clear';
$logs = 'logs/' . $name . '.out';
- $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name );
+ $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name);
$job->at($at);
$job->output($logs);
$job->backlink('/config/system#caching');
-
}
-
-
}
diff --git a/system/src/Grav/Common/Composer.php b/system/src/Grav/Common/Composer.php
index c6abb2c6..f1f6e5d3 100644
--- a/system/src/Grav/Common/Composer.php
+++ b/system/src/Grav/Common/Composer.php
@@ -3,12 +3,18 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use function function_exists;
+
+/**
+ * Class Composer
+ * @package Grav\Common
+ */
class Composer
{
/** @const Default composer location */
@@ -21,12 +27,12 @@ class Composer
*/
public static function getComposerLocation()
{
- if (!\function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
+ if (!function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
return self::DEFAULT_PATH;
}
// check for global composer install
- $path = trim(shell_exec('command -v composer'));
+ $path = trim((string)shell_exec('command -v composer'));
// fall back to grav bundled composer
if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
diff --git a/system/src/Grav/Common/Config/CompiledBase.php b/system/src/Grav/Common/Config/CompiledBase.php
index 3e2022f0..0764189d 100644
--- a/system/src/Grav/Common/Config/CompiledBase.php
+++ b/system/src/Grav/Common/Config/CompiledBase.php
@@ -3,78 +3,70 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Config;
+use BadMethodCallException;
+use Exception;
use RocketTheme\Toolbox\File\PhpFile;
+use RuntimeException;
+use function get_class;
+use function is_array;
+/**
+ * Class CompiledBase
+ * @package Grav\Common\Config
+ */
abstract class CompiledBase
{
- /**
- * @var int Version number for the compiled file.
- */
+ /** @var int Version number for the compiled file. */
public $version = 1;
- /**
- * @var string Filename (base name) of the compiled configuration.
- */
+ /** @var string Filename (base name) of the compiled configuration. */
public $name;
- /**
- * @var string|bool Configuration checksum.
- */
+ /** @var string|bool Configuration checksum. */
public $checksum;
- /**
- * @var string Timestamp of compiled configuration
- */
- public $timestamp;
+ /** @var int Timestamp of compiled configuration */
+ public $timestamp = 0;
- /**
- * @var string Cache folder to be used.
- */
+ /** @var string Cache folder to be used. */
protected $cacheFolder;
- /**
- * @var array List of files to load.
- */
+ /** @var array List of files to load. */
protected $files;
- /**
- * @var string
- */
+ /** @var string */
protected $path;
- /**
- * @var mixed Configuration object.
- */
+ /** @var mixed Configuration object. */
protected $object;
/**
* @param string $cacheFolder Cache folder to be used.
* @param array $files List of files as returned from ConfigFileFinder class.
* @param string $path Base path for the file list.
- * @throws \BadMethodCallException
+ * @throws BadMethodCallException
*/
public function __construct($cacheFolder, array $files, $path)
{
if (!$cacheFolder) {
- throw new \BadMethodCallException('Cache folder not defined.');
+ throw new BadMethodCallException('Cache folder not defined.');
}
$this->path = $path ? rtrim($path, '\\/') . '/' : '';
$this->cacheFolder = $cacheFolder;
$this->files = $files;
- $this->timestamp = 0;
}
/**
* Get filename for the compiled PHP file.
*
- * @param string $name
+ * @param string|null $name
* @return $this
*/
public function name($name = null)
@@ -88,8 +80,12 @@ abstract class CompiledBase
/**
* Function gets called when cached configuration is saved.
+ *
+ * @return void
*/
- public function modified() {}
+ public function modified()
+ {
+ }
/**
* Get timestamp of compiled configuration
@@ -136,6 +132,9 @@ abstract class CompiledBase
return $this->checksum;
}
+ /**
+ * @return string
+ */
protected function createFilename()
{
return "{$this->cacheFolder}/{$this->name()->name}.php";
@@ -145,11 +144,14 @@ abstract class CompiledBase
* Create configuration object.
*
* @param array $data
+ * @return void
*/
abstract protected function createObject(array $data = []);
/**
* Finalize configuration object.
+ *
+ * @return void
*/
abstract protected function finalizeObject();
@@ -157,7 +159,8 @@ abstract class CompiledBase
* Load single configuration file and append it to the correct position.
*
* @param string $name Name of the position.
- * @param string $filename File to be loaded.
+ * @param string|string[] $filename File(s) to be loaded.
+ * @return void
*/
abstract protected function loadFile($name, $filename);
@@ -197,10 +200,9 @@ abstract class CompiledBase
}
$cache = include $filename;
- if (
- !\is_array($cache)
+ if (!is_array($cache)
|| !isset($cache['checksum'], $cache['data'], $cache['@class'])
- || $cache['@class'] !== \get_class($this)
+ || $cache['@class'] !== get_class($this)
) {
return false;
}
@@ -222,7 +224,8 @@ abstract class CompiledBase
* Save compiled file.
*
* @param string $filename
- * @throws \RuntimeException
+ * @return void
+ * @throws RuntimeException
* @internal
*/
protected function saveCompiledFile($filename)
@@ -232,7 +235,7 @@ abstract class CompiledBase
// Attempt to lock the file for writing.
try {
$file->lock(false);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// Another process has locked the file; we will check this in a bit.
}
@@ -242,7 +245,7 @@ abstract class CompiledBase
}
$cache = [
- '@class' => \get_class($this),
+ '@class' => get_class($this),
'timestamp' => time(),
'checksum' => $this->checksum(),
'files' => $this->files,
@@ -256,6 +259,9 @@ abstract class CompiledBase
$this->modified();
}
+ /**
+ * @return array
+ */
protected function getState()
{
return $this->object->toArray();
diff --git a/system/src/Grav/Common/Config/CompiledBlueprints.php b/system/src/Grav/Common/Config/CompiledBlueprints.php
index 9ad7be6f..77430542 100644
--- a/system/src/Grav/Common/Config/CompiledBlueprints.php
+++ b/system/src/Grav/Common/Config/CompiledBlueprints.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -19,6 +19,12 @@ use Grav\Common\Grav;
*/
class CompiledBlueprints extends CompiledBase
{
+ /**
+ * CompiledBlueprints constructor.
+ * @param string $cacheFolder
+ * @param array $files
+ * @param string $path
+ */
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
@@ -45,7 +51,7 @@ class CompiledBlueprints extends CompiledBase
/**
* Create configuration object.
*
- * @param array $data
+ * @param array $data
*/
protected function createObject(array $data = [])
{
@@ -64,6 +70,8 @@ class CompiledBlueprints extends CompiledBase
/**
* Finalize configuration object.
+ *
+ * @return void
*/
protected function finalizeObject()
{
@@ -74,6 +82,7 @@ class CompiledBlueprints extends CompiledBase
*
* @param string $name Name of the position.
* @param array $files Files to be loaded.
+ * @return void
*/
protected function loadFile($name, $files)
{
@@ -112,6 +121,9 @@ class CompiledBlueprints extends CompiledBase
return true;
}
+ /**
+ * @return array
+ */
protected function getState()
{
return $this->object->getState();
diff --git a/system/src/Grav/Common/Config/CompiledConfig.php b/system/src/Grav/Common/Config/CompiledConfig.php
index 1c92edd9..22225bce 100644
--- a/system/src/Grav/Common/Config/CompiledConfig.php
+++ b/system/src/Grav/Common/Config/CompiledConfig.php
@@ -3,26 +3,33 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Config;
use Grav\Common\File\CompiledYamlFile;
+use function is_callable;
+/**
+ * Class CompiledConfig
+ * @package Grav\Common\Config
+ */
class CompiledConfig extends CompiledBase
{
- /**
- * @var callable Blueprints loader.
- */
+ /** @var callable Blueprints loader. */
protected $callable;
+ /** @var bool */
+ protected $withDefaults = false;
+
/**
- * @var bool
+ * CompiledConfig constructor.
+ * @param string $cacheFolder
+ * @param array $files
+ * @param string $path
*/
- protected $withDefaults;
-
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
@@ -58,10 +65,11 @@ class CompiledConfig extends CompiledBase
* Create configuration object.
*
* @param array $data
+ * @return void
*/
protected function createObject(array $data = [])
{
- if ($this->withDefaults && empty($data) && \is_callable($this->callable)) {
+ if ($this->withDefaults && empty($data) && is_callable($this->callable)) {
$blueprints = $this->callable;
$data = $blueprints()->getDefaults();
}
@@ -71,6 +79,8 @@ class CompiledConfig extends CompiledBase
/**
* Finalize configuration object.
+ *
+ * @return void
*/
protected function finalizeObject()
{
@@ -80,6 +90,8 @@ class CompiledConfig extends CompiledBase
/**
* Function gets called when cached configuration is saved.
+ *
+ * @return void
*/
public function modified()
{
@@ -91,6 +103,7 @@ class CompiledConfig extends CompiledBase
*
* @param string $name Name of the position.
* @param string $filename File to be loaded.
+ * @return void
*/
protected function loadFile($name, $filename)
{
diff --git a/system/src/Grav/Common/Config/CompiledLanguages.php b/system/src/Grav/Common/Config/CompiledLanguages.php
index ef18145c..6674389a 100644
--- a/system/src/Grav/Common/Config/CompiledLanguages.php
+++ b/system/src/Grav/Common/Config/CompiledLanguages.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,8 +11,18 @@ namespace Grav\Common\Config;
use Grav\Common\File\CompiledYamlFile;
+/**
+ * Class CompiledLanguages
+ * @package Grav\Common\Config
+ */
class CompiledLanguages extends CompiledBase
{
+ /**
+ * CompiledLanguages constructor.
+ * @param string $cacheFolder
+ * @param array $files
+ * @param string $path
+ */
public function __construct($cacheFolder, array $files, $path)
{
parent::__construct($cacheFolder, $files, $path);
@@ -24,6 +34,7 @@ class CompiledLanguages extends CompiledBase
* Create configuration object.
*
* @param array $data
+ * @return void
*/
protected function createObject(array $data = [])
{
@@ -32,6 +43,8 @@ class CompiledLanguages extends CompiledBase
/**
* Finalize configuration object.
+ *
+ * @return void
*/
protected function finalizeObject()
{
@@ -42,6 +55,8 @@ class CompiledLanguages extends CompiledBase
/**
* Function gets called when cached configuration is saved.
+ *
+ * @return void
*/
public function modified()
{
@@ -53,6 +68,7 @@ class CompiledLanguages extends CompiledBase
*
* @param string $name Name of the position.
* @param string $filename File to be loaded.
+ * @return void
*/
protected function loadFile($name, $filename)
{
diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php
index 7b808463..7adb523e 100644
--- a/system/src/Grav/Common/Config/Config.php
+++ b/system/src/Grav/Common/Config/Config.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,9 +14,15 @@ use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Common\Service\ConfigServiceProvider;
use Grav\Common\Utils;
+use function is_array;
+/**
+ * Class Config
+ * @package Grav\Common\Config
+ */
class Config extends Data
{
+ /** @var string */
public $environment;
/** @var string */
@@ -28,6 +34,9 @@ class Config extends Data
/** @var bool */
protected $modified = false;
+ /**
+ * @return string
+ */
public function key()
{
if (null === $this->key) {
@@ -37,6 +46,10 @@ class Config extends Data
return $this->key;
}
+ /**
+ * @param string|null $checksum
+ * @return string|null
+ */
public function checksum($checksum = null)
{
if ($checksum !== null) {
@@ -46,6 +59,10 @@ class Config extends Data
return $this->checksum;
}
+ /**
+ * @param bool|null $modified
+ * @return bool
+ */
public function modified($modified = null)
{
if ($modified !== null) {
@@ -55,6 +72,10 @@ class Config extends Data
return $this->modified;
}
+ /**
+ * @param int|null $timestamp
+ * @return int
+ */
public function timestamp($timestamp = null)
{
if ($timestamp !== null) {
@@ -64,6 +85,9 @@ class Config extends Data
return $this->timestamp;
}
+ /**
+ * @return $this
+ */
public function reload()
{
$grav = Grav::instance();
@@ -86,6 +110,9 @@ class Config extends Data
return $this;
}
+ /**
+ * @return void
+ */
public function debug()
{
/** @var Debugger $debugger */
@@ -97,11 +124,14 @@ class Config extends Data
}
}
+ /**
+ * @return void
+ */
public function init()
{
$setup = Grav::instance()['setup']->toArray();
foreach ($setup as $key => $value) {
- if ($key === 'streams' || !\is_array($value)) {
+ if ($key === 'streams' || !is_array($value)) {
// Optimized as streams and simple values are fully defined in setup.
$this->items[$key] = $value;
} else {
diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php
index 1e7adca5..e6b1afea 100644
--- a/system/src/Grav/Common/Config/ConfigFileFinder.php
+++ b/system/src/Grav/Common/Config/ConfigFileFinder.php
@@ -3,16 +3,23 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Config;
+use DirectoryIterator;
use Grav\Common\Filesystem\Folder;
+use RecursiveDirectoryIterator;
+/**
+ * Class ConfigFileFinder
+ * @package Grav\Common\Config
+ */
class ConfigFileFinder
{
+ /** @var string */
protected $base = '';
/**
@@ -40,6 +47,7 @@ class ConfigFileFinder
foreach ($paths as $folder) {
$list += $this->detectRecursive($folder, $pattern, $levels);
}
+
return $list;
}
@@ -61,6 +69,7 @@ class ConfigFileFinder
$list += $files[trim($path, '/')];
}
+
return $list;
}
@@ -78,6 +87,7 @@ class ConfigFileFinder
foreach ($paths as $folder) {
$list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels));
}
+
return $list;
}
@@ -96,6 +106,7 @@ class ConfigFileFinder
foreach ($folders as $folder) {
$list += $this->detectInFolder($folder, $filename);
}
+
return $list;
}
@@ -103,7 +114,7 @@ class ConfigFileFinder
* Find filename from a list of folders.
*
* @param array $folders
- * @param string $filename
+ * @param string|null $filename
* @return array
*/
public function locateInFolders(array $folders, $filename = null)
@@ -113,6 +124,7 @@ class ConfigFileFinder
$path = trim(Folder::getRelativePath($folder), '/');
$list[$path] = $this->detectInFolder($folder, $filename);
}
+
return $list;
}
@@ -166,7 +178,7 @@ class ConfigFileFinder
'filters' => [
'pre-key' => $this->base,
'key' => $pattern,
- 'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
+ 'value' => function (RecursiveDirectoryIterator $file) use ($path) {
return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()];
}
],
@@ -187,7 +199,7 @@ class ConfigFileFinder
* Detects all directories with the lookup file and returns them with last modification time.
*
* @param string $folder Location to look up from.
- * @param string $lookup Filename to be located (defaults to directory name).
+ * @param string|null $lookup Filename to be located (defaults to directory name).
* @return array
* @internal
*/
@@ -200,9 +212,7 @@ class ConfigFileFinder
$list = [];
if (is_dir($folder)) {
- $iterator = new \DirectoryIterator($folder);
-
- /** @var \DirectoryIterator $directory */
+ $iterator = new DirectoryIterator($folder);
foreach ($iterator as $directory) {
if (!$directory->isDir() || $directory->isDot()) {
continue;
@@ -244,7 +254,7 @@ class ConfigFileFinder
'filters' => [
'pre-key' => $this->base,
'key' => $pattern,
- 'value' => function (\RecursiveDirectoryIterator $file) use ($path) {
+ 'value' => function (RecursiveDirectoryIterator $file) use ($path) {
return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()];
}
],
diff --git a/system/src/Grav/Common/Config/Languages.php b/system/src/Grav/Common/Config/Languages.php
index 9c76dc56..4a863a1b 100644
--- a/system/src/Grav/Common/Config/Languages.php
+++ b/system/src/Grav/Common/Config/Languages.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,24 +12,25 @@ namespace Grav\Common\Config;
use Grav\Common\Data\Data;
use Grav\Common\Utils;
+/**
+ * Class Languages
+ * @package Grav\Common\Config
+ */
class Languages extends Data
{
- /**
- * @var string|null
- */
+ /** @var string|null */
protected $checksum;
- /**
- * @var string|null
- */
- protected $modified;
+ /** @var bool */
+ protected $modified = false;
+
+ /** @var int */
+ protected $timestamp = 0;
/**
- * @var string|null
+ * @param string|null $checksum
+ * @return string|null
*/
- protected $timestamp;
-
-
public function checksum($checksum = null)
{
if ($checksum !== null) {
@@ -39,6 +40,10 @@ class Languages extends Data
return $this->checksum;
}
+ /**
+ * @param bool|null $modified
+ * @return bool
+ */
public function modified($modified = null)
{
if ($modified !== null) {
@@ -48,6 +53,10 @@ class Languages extends Data
return $this->modified;
}
+ /**
+ * @param int|null $timestamp
+ * @return int
+ */
public function timestamp($timestamp = null)
{
if ($timestamp !== null) {
@@ -57,6 +66,9 @@ class Languages extends Data
return $this->timestamp;
}
+ /**
+ * @return void
+ */
public function reformat()
{
if (isset($this->items['plugins'])) {
@@ -65,17 +77,29 @@ class Languages extends Data
}
}
+ /**
+ * @param array $data
+ * @return void
+ */
public function mergeRecursive(array $data)
{
$this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
}
+ /**
+ * @param string $lang
+ * @return array
+ */
public function flattenByLang($lang)
{
$language = $this->items[$lang];
return Utils::arrayFlattenDotNotation($language);
}
+ /**
+ * @param array $array
+ * @return array
+ */
public function unflatten($array)
{
return Utils::arrayUnflattenDotNotation($array);
diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php
index 6a4d336b..6ff7372d 100644
--- a/system/src/Grav/Common/Config/Setup.php
+++ b/system/src/Grav/Common/Config/Setup.php
@@ -3,19 +3,28 @@
/**
* @package Grav\Common\Config
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Config;
+use BadMethodCallException;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Data\Data;
use Grav\Common\Utils;
+use InvalidArgumentException;
use Pimple\Container;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use function defined;
+use function is_array;
+/**
+ * Class Setup
+ * @package Grav\Common\Config
+ */
class Setup extends Data
{
/**
@@ -28,28 +37,61 @@ class Setup extends Data
];
/**
- * @var string Current environment normalized to lower case.
+ * @var string|null Current environment normalized to lower case.
*/
public static $environment;
+ /** @var string */
+ public static $securityFile = 'config://security.yaml';
+
+ /** @var array */
protected $streams = [
- 'system' => [
+ 'user' => [
'type' => 'ReadOnlyStream',
+ 'force' => true,
'prefixes' => [
- '' => ['system'],
+ '' => [] // Set in constructor
]
],
- 'user' => [
- 'type' => 'ReadOnlyStream',
+ 'cache' => [
+ 'type' => 'Stream',
+ 'force' => true,
+ 'prefixes' => [
+ '' => [], // Set in constructor
+ 'images' => ['images']
+ ]
+ ],
+ 'log' => [
+ 'type' => 'Stream',
'force' => true,
'prefixes' => [
- '' => ['user'],
+ '' => [] // Set in constructor
+ ]
+ ],
+ 'tmp' => [
+ 'type' => 'Stream',
+ 'force' => true,
+ 'prefixes' => [
+ '' => [] // Set in constructor
+ ]
+ ],
+ 'backup' => [
+ 'type' => 'Stream',
+ 'force' => true,
+ 'prefixes' => [
+ '' => [] // Set in constructor
]
],
'environment' => [
'type' => 'ReadOnlyStream'
// If not defined, environment will be set up in the constructor.
],
+ 'system' => [
+ 'type' => 'ReadOnlyStream',
+ 'prefixes' => [
+ '' => ['system'],
+ ]
+ ],
'asset' => [
'type' => 'Stream',
'prefixes' => [
@@ -59,13 +101,13 @@ class Setup extends Data
'blueprints' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
- '' => ['environment://blueprints', 'user://blueprints', 'system/blueprints'],
+ '' => ['environment://blueprints', 'user://blueprints', 'system://blueprints'],
]
],
'config' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
- '' => ['environment://config', 'user://config', 'system/config'],
+ '' => ['environment://config', 'user://config', 'system://config'],
]
],
'plugins' => [
@@ -89,36 +131,7 @@ class Setup extends Data
'languages' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
- '' => ['environment://languages', 'user://languages', 'system/languages'],
- ]
- ],
- 'cache' => [
- 'type' => 'Stream',
- 'force' => true,
- 'prefixes' => [
- '' => ['cache'],
- 'images' => ['images']
- ]
- ],
- 'log' => [
- 'type' => 'Stream',
- 'force' => true,
- 'prefixes' => [
- '' => ['logs']
- ]
- ],
- 'backup' => [
- 'type' => 'Stream',
- 'force' => true,
- 'prefixes' => [
- '' => ['backup']
- ]
- ],
- 'tmp' => [
- 'type' => 'Stream',
- 'force' => true,
- 'prefixes' => [
- '' => ['tmp']
+ '' => ['environment://languages', 'user://languages', 'system://languages'],
]
],
'image' => [
@@ -153,27 +166,58 @@ class Setup extends Data
*/
public function __construct($container)
{
+ // Configure main streams.
+ $abs = str_starts_with(GRAV_SYSTEM_PATH, '/');
+ $this->streams['system']['prefixes'][''] = $abs ? ['system', GRAV_SYSTEM_PATH] : ['system'];
+ $this->streams['user']['prefixes'][''] = [GRAV_USER_PATH];
+ $this->streams['cache']['prefixes'][''] = [GRAV_CACHE_PATH];
+ $this->streams['log']['prefixes'][''] = [GRAV_LOG_PATH];
+ $this->streams['tmp']['prefixes'][''] = [GRAV_TMP_PATH];
+ $this->streams['backup']['prefixes'][''] = [GRAV_BACKUP_PATH];
+
+ // If environment is not set, look for the environment variable and then the constant.
+ $environment = static::$environment ??
+ (defined('GRAV_ENVIRONMENT') ? GRAV_ENVIRONMENT : (getenv('GRAV_ENVIRONMENT') ?: null));
+
// If no environment is set, make sure we get one (CLI or hostname).
- if (!static::$environment) {
- if (\defined('GRAV_CLI')) {
- static::$environment = 'cli';
+ if (null === $environment) {
+ if (defined('GRAV_CLI')) {
+ $environment = 'cli';
} else {
/** @var ServerRequestInterface $request */
$request = $container['request'];
$host = $request->getUri()->getHost();
- static::$environment = Utils::substrToString($host, ':');
+ $environment = Utils::substrToString($host, ':');
}
}
// Resolve server aliases to the proper environment.
- $environment = $this->environments[static::$environment] ?? static::$environment;
+ static::$environment = static::$environments[$environment] ?? $environment;
// Pre-load setup.php which contains our initial configuration.
// Configuration may contain dynamic parts, which is why we need to always load it.
- // If "GRAV_SETUP_PATH" has been defined, use it, otherwise use defaults.
- $file = \defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php';
- $setup = is_file($file) ? (array) include $file : [];
+ // If GRAV_SETUP_PATH has been defined, use it, otherwise use defaults.
+ $setupFile = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : (getenv('GRAV_SETUP_PATH') ?: null);
+ if (null !== $setupFile) {
+ // Make sure that the custom setup file exists. Terminates the script if not.
+ if (!str_starts_with($setupFile, '/')) {
+ $setupFile = GRAV_WEBROOT . '/' . $setupFile;
+ }
+ if (!is_file($setupFile)) {
+ echo 'GRAV_SETUP_PATH is defined but does not point to existing setup file.';
+ exit(1);
+ }
+ } else {
+ $setupFile = GRAV_WEBROOT . '/setup.php';
+ if (!is_file($setupFile)) {
+ $setupFile = GRAV_WEBROOT . '/' . GRAV_USER_PATH . '/setup.php';
+ }
+ if (!is_file($setupFile)) {
+ $setupFile = null;
+ }
+ }
+ $setup = $setupFile ? (array) include $setupFile : [];
// Add default streams defined in beginning of the class.
if (!isset($setup['streams']['schemes'])) {
@@ -184,19 +228,41 @@ class Setup extends Data
// Initialize class.
parent::__construct($setup);
+ $this->def('environment', static::$environment);
+
+ // Figure out path for the current environment.
+ $envPath = defined('GRAV_ENVIRONMENT_PATH') ? GRAV_ENVIRONMENT_PATH : (getenv('GRAV_ENVIRONMENT_PATH') ?: null);
+ if (null === $envPath) {
+ // Find common path for all environments and append current environment into it.
+ $envPath = defined('GRAV_ENVIRONMENTS_PATH') ? GRAV_ENVIRONMENTS_PATH : (getenv('GRAV_ENVIRONMENTS_PATH') ?: null);
+ if (null !== $envPath) {
+ $envPath .= '/';
+ } else {
+ // Use default location. Start with Grav 1.7 default.
+ $envPath = GRAV_WEBROOT. '/' . GRAV_USER_PATH . '/env';
+ if (is_dir($envPath)) {
+ $envPath = 'user://env/';
+ } else {
+ // Fallback to Grav 1.6 default.
+ $envPath = 'user://';
+ }
+ }
+ $envPath .= $this->get('environment');
+ }
+
// Set up environment.
- $this->def('environment', $environment);
- $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->get('environment')}"]]);
+ $this->def('environment', static::$environment);
+ $this->def('streams.schemes.environment.prefixes', ['' => [$envPath]]);
}
/**
* @return $this
- * @throws \RuntimeException
- * @throws \InvalidArgumentException
+ * @throws RuntimeException
+ * @throws InvalidArgumentException
*/
public function init()
{
- $locator = new UniformResourceLocator(GRAV_ROOT);
+ $locator = new UniformResourceLocator(GRAV_WEBROOT);
$files = [];
$guard = 5;
@@ -220,7 +286,7 @@ class Setup extends Data
} while (--$guard);
if (!$guard) {
- throw new \RuntimeException('Setup: Configuration reload loop detected!');
+ throw new RuntimeException('Setup: Configuration reload loop detected!');
}
// Make sure we have valid setup.
@@ -233,7 +299,8 @@ class Setup extends Data
* Initialize resource locator by using the configuration.
*
* @param UniformResourceLocator $locator
- * @throws \BadMethodCallException
+ * @return void
+ * @throws BadMethodCallException
*/
public function initializeLocator(UniformResourceLocator $locator)
{
@@ -279,32 +346,66 @@ class Setup extends Data
/**
* @param UniformResourceLocator $locator
- * @throws \InvalidArgumentException
- * @throws \BadMethodCallException
- * @throws \RuntimeException
+ * @return void
+ * @throws InvalidArgumentException
+ * @throws BadMethodCallException
+ * @throws RuntimeException
*/
protected function check(UniformResourceLocator $locator)
{
$streams = $this->items['streams']['schemes'] ?? null;
- if (!\is_array($streams)) {
- throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
+ if (!is_array($streams)) {
+ throw new InvalidArgumentException('Configuration is missing streams.schemes!');
}
$diff = array_keys(array_diff_key($this->streams, $streams));
if ($diff) {
- throw new \InvalidArgumentException(
+ throw new InvalidArgumentException(
sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff))
);
}
try {
+ // If environment is found, remove all missing override locations (B/C compatibility).
+ if ($locator->findResource('environment://', true)) {
+ $force = $this->get('streams.schemes.environment.force', false);
+ if (!$force) {
+ $prefixes = $this->get('streams.schemes.environment.prefixes.');
+ $update = false;
+ foreach ($prefixes as $i => $prefix) {
+ if ($locator->isStream($prefix)) {
+ if ($locator->findResource($prefix, true)) {
+ break;
+ }
+ } elseif (file_exists($prefix)) {
+ break;
+ }
+
+ unset($prefixes[$i]);
+ $update = true;
+ }
+
+ if ($update) {
+ $this->set('streams.schemes.environment.prefixes', ['' => array_values($prefixes)]);
+ $this->initializeLocator($locator);
+ }
+ }
+ }
+
if (!$locator->findResource('environment://config', true)) {
// If environment does not have its own directory, remove it from the lookup.
- $this->set('streams.schemes.environment.prefixes', ['config' => []]);
+ $prefixes = $this->get('streams.schemes.environment.prefixes');
+ $prefixes['config'] = [];
+
+ $this->set('streams.schemes.environment.prefixes', $prefixes);
$this->initializeLocator($locator);
}
- // Create security.yaml if it doesn't exist.
- $filename = $locator->findResource('config://security.yaml', true, true);
+ // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
+ $securityFile = basename(static::$securityFile);
+ $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
+ $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
+ $filename = "{$securityFolder}/{$securityFile}";
+
$security_file = CompiledYamlFile::instance($filename);
$security_content = (array)$security_file->content();
@@ -314,8 +415,8 @@ class Setup extends Data
$security_file->save();
$security_file->free();
}
- } catch (\RuntimeException $e) {
- throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
+ } catch (RuntimeException $e) {
+ throw new RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
}
}
}
diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php
index e7037774..28781993 100644
--- a/system/src/Grav/Common/Data/Blueprint.php
+++ b/system/src/Grav/Common/Data/Blueprint.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,34 +14,69 @@ use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use RocketTheme\Toolbox\Blueprints\BlueprintForm;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use function call_user_func_array;
+use function count;
+use function function_exists;
+use function in_array;
+use function is_array;
+use function is_int;
+use function is_object;
+use function is_string;
+use function strlen;
+/**
+ * Class Blueprint
+ * @package Grav\Common\Data
+ */
class Blueprint extends BlueprintForm
{
/** @var string */
protected $context = 'blueprints://';
+ /** @var string|null */
protected $scope;
- /** @var BlueprintSchema */
+ /** @var BlueprintSchema|null */
protected $blueprintSchema;
- /** @var array */
+ /** @var object|null */
+ protected $object;
+
+ /** @var array|null */
protected $defaults;
+ /** @var array */
protected $handlers = [];
+ /**
+ * Clone blueprint.
+ */
public function __clone()
{
- if ($this->blueprintSchema) {
+ if (null !== $this->blueprintSchema) {
$this->blueprintSchema = clone $this->blueprintSchema;
}
}
+ /**
+ * @param string $scope
+ * @return void
+ */
public function setScope($scope)
{
$this->scope = $scope;
}
+ /**
+ * @param object $object
+ * @return void
+ */
+ public function setObject($object)
+ {
+ $this->object = $object;
+ }
+
/**
* Set default values for field types.
*
@@ -57,6 +92,29 @@ class Blueprint extends BlueprintForm
return $this;
}
+ /**
+ * @param string $name
+ * @return array|mixed|null
+ * @since 1.7
+ */
+ public function getDefaultValue(string $name)
+ {
+ $path = explode('.', $name) ?: [];
+ $current = $this->getDefaults();
+
+ foreach ($path as $field) {
+ if (is_object($current) && isset($current->{$field})) {
+ $current = $current->{$field};
+ } elseif (is_array($current) && isset($current[$field])) {
+ $current = $current[$field];
+ } else {
+ return null;
+ }
+ }
+
+ return $current;
+ }
+
/**
* Get nested structure containing default values defined in the blueprints.
*
@@ -88,7 +146,7 @@ class Blueprint extends BlueprintForm
$current = &$this->items;
foreach ($path as $field) {
- if (\is_object($current)) {
+ if (is_object($current)) {
// Handle objects.
if (!isset($current->{$field})) {
$current->{$field} = [];
@@ -97,7 +155,7 @@ class Blueprint extends BlueprintForm
$current = &$current->{$field};
} else {
// Handle arrays and scalars.
- if (!\is_array($current)) {
+ if (!is_array($current)) {
$current = [$field => []];
} elseif (!isset($current[$field])) {
$current[$field] = [];
@@ -111,6 +169,7 @@ class Blueprint extends BlueprintForm
foreach ($data as $property => $call) {
$action = $call['action'];
$method = 'dynamic' . ucfirst($action);
+ $call['object'] = $this->object;
if (isset($this->handlers[$action])) {
$callable = $this->handlers[$action];
@@ -124,12 +183,44 @@ class Blueprint extends BlueprintForm
return $this;
}
+ /**
+ * Extend blueprint with another blueprint.
+ *
+ * @param BlueprintForm|array $extends
+ * @param bool $append
+ * @return $this
+ */
+ public function extend($extends, $append = false)
+ {
+ parent::extend($extends, $append);
+
+ $this->deepInit($this->items);
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ * @param string $separator
+ * @param bool $append
+ * @return $this
+ */
+ public function embed($name, $value, $separator = '/', $append = false)
+ {
+ parent::embed($name, $value, $separator, $append);
+
+ $this->deepInit($this->items);
+
+ return $this;
+ }
+
/**
* Merge two arrays by using blueprints.
*
* @param array $data1
* @param array $data2
- * @param string $name Optional
+ * @param string|null $name Optional
* @param string $separator Optional
* @return array
*/
@@ -172,13 +263,15 @@ class Blueprint extends BlueprintForm
* Validate data against blueprints.
*
* @param array $data
- * @throws \RuntimeException
+ * @param array $options
+ * @return void
+ * @throws RuntimeException
*/
- public function validate(array $data)
+ public function validate(array $data, array $options = [])
{
$this->initInternals();
- $this->blueprintSchema->validate($data);
+ $this->blueprintSchema->validate($data, $options);
}
/**
@@ -193,7 +286,7 @@ class Blueprint extends BlueprintForm
{
$this->initInternals();
- return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues);
+ return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues) ?? [];
}
@@ -201,13 +294,14 @@ class Blueprint extends BlueprintForm
* Flatten data by using blueprints.
*
* @param array $data
+ * @param bool $includeAll
* @return array
*/
- public function flattenData(array $data)
+ public function flattenData(array $data, bool $includeAll = false)
{
$this->initInternals();
- return $this->blueprintSchema->flattenData($data);
+ return $this->blueprintSchema->flattenData($data, $includeAll);
}
@@ -223,6 +317,11 @@ class Blueprint extends BlueprintForm
return $this->blueprintSchema;
}
+ /**
+ * @param string $name
+ * @param callable $callable
+ * @return void
+ */
public function addDynamicHandler(string $name, callable $callable): void
{
$this->handlers[$name] = $callable;
@@ -230,6 +329,8 @@ class Blueprint extends BlueprintForm
/**
* Initialize validator.
+ *
+ * @return void
*/
protected function initInternals()
{
@@ -250,12 +351,12 @@ class Blueprint extends BlueprintForm
/**
* @param string $filename
- * @return string
+ * @return array
*/
protected function loadFile($filename)
{
$file = CompiledYamlFile::instance($filename);
- $content = $file->content();
+ $content = (array)$file->content();
$file->free();
return $content;
@@ -263,7 +364,7 @@ class Blueprint extends BlueprintForm
/**
* @param string|array $path
- * @param string $context
+ * @param string|null $context
* @return array
*/
protected function getFiles($path, $context = null)
@@ -271,16 +372,24 @@ class Blueprint extends BlueprintForm
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- if (\is_string($path) && !$locator->isStream($path)) {
+ if (is_string($path) && !$locator->isStream($path)) {
+ if (is_file($path)) {
+ return [$path];
+ }
+
// Find path overrides.
- $paths = (array) ($this->overrides[$path] ?? null);
+ if (null === $context) {
+ $paths = (array) ($this->overrides[$path] ?? null);
+ } else {
+ $paths = [];
+ }
// Add path pointing to default context.
if ($context === null) {
$context = $this->context;
}
- if ($context && $context[\strlen($context)-1] !== '/') {
+ if ($context && $context[strlen($context)-1] !== '/') {
$context .= '/';
}
@@ -297,7 +406,7 @@ class Blueprint extends BlueprintForm
$files = [];
foreach ($paths as $lookup) {
- if (\is_string($lookup) && strpos($lookup, '://')) {
+ if (is_string($lookup) && strpos($lookup, '://')) {
$files = array_merge($files, $locator->findResources($lookup));
} else {
$files[] = $lookup;
@@ -311,12 +420,13 @@ class Blueprint extends BlueprintForm
* @param array $field
* @param string $property
* @param array $call
+ * @return void
*/
protected function dynamicData(array &$field, $property, array &$call)
{
$params = $call['params'];
- if (\is_array($params)) {
+ if (is_array($params)) {
$function = array_shift($params);
} else {
$function = $params;
@@ -327,18 +437,18 @@ class Blueprint extends BlueprintForm
$data = null;
if (!$f) {
- if (\function_exists($o)) {
- $data = \call_user_func_array($o, $params);
+ if (function_exists($o)) {
+ $data = call_user_func_array($o, $params);
}
} else {
if (method_exists($o, $f)) {
- $data = \call_user_func_array([$o, $f], $params);
+ $data = call_user_func_array([$o, $f], $params);
}
}
// If function returns a value,
if (null !== $data) {
- if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
+ if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
// Combine field and @data-field together.
$field[$property] += $data;
} else {
@@ -352,15 +462,33 @@ class Blueprint extends BlueprintForm
* @param array $field
* @param string $property
* @param array $call
+ * @return void
*/
protected function dynamicConfig(array &$field, $property, array &$call)
{
- $value = $call['params'];
+ $params = $call['params'];
+ if (is_array($params)) {
+ $value = array_shift($params);
+ $params = array_shift($params);
+ } else {
+ $value = $params;
+ $params = [];
+ }
+
$default = $field[$property] ?? null;
$config = Grav::instance()['config']->get($value, $default);
+ if (!empty($field['value_only'])) {
+ $config = array_combine($config, $config);
+ }
if (null !== $config) {
- $field[$property] = $config;
+ if (!empty($params['append']) && is_array($config) && isset($field[$property]) && is_array($field[$property])) {
+ // Combine field and @config-field together.
+ $field[$property] += $config;
+ } else {
+ // Or create/replace field with @config-field.
+ $field[$property] = $config;
+ }
}
}
@@ -368,6 +496,7 @@ class Blueprint extends BlueprintForm
* @param array $field
* @param string $property
* @param array $call
+ * @return void
*/
protected function dynamicSecurity(array &$field, $property, array &$call)
{
@@ -380,18 +509,48 @@ class Blueprint extends BlueprintForm
/** @var UserInterface|null $user */
$user = $grav['user'] ?? null;
- foreach ($actions as $action) {
- if (!$user || !$user->authorize($action)) {
- $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
- return;
+ $success = null !== $user;
+ if ($success) {
+ $success = $this->resolveActions($user, $actions);
+ }
+ if (!$success) {
+ $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+ }
+ }
+
+ /**
+ * @param UserInterface|null $user
+ * @param array $actions
+ * @param string $op
+ * @return bool
+ */
+ protected function resolveActions(?UserInterface $user, array $actions, string $op = 'and')
+ {
+ if (null === $user) {
+ return false;
+ }
+
+ $c = $i = count($actions);
+ foreach ($actions as $key => $action) {
+ if (!is_int($key) && is_array($actions)) {
+ $i -= $this->resolveActions($user, $action, $key);
+ } elseif ($user->authorize($action)) {
+ $i--;
}
}
+
+ if ($op === 'and') {
+ return $i === 0;
+ }
+
+ return $c !== $i;
}
/**
* @param array $field
* @param string $property
* @param array $call
+ * @return void
*/
protected function dynamicScope(array &$field, $property, array &$call)
{
@@ -400,7 +559,7 @@ class Blueprint extends BlueprintForm
}
$scopes = (array)$call['params'];
- $matches = \in_array($this->scope, $scopes, true);
+ $matches = in_array($this->scope, $scopes, true);
if ($this->scope && $property !== 'ignore') {
$matches = !$matches;
}
@@ -411,9 +570,15 @@ class Blueprint extends BlueprintForm
}
}
+ /**
+ * @param array $field
+ * @param string $property
+ * @param mixed $value
+ * @return void
+ */
protected function addPropertyRecursive(array &$field, $property, $value)
{
- if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
+ if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
} else {
$field[$property] = $value;
diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php
index c79b3fd1..7990bec2 100644
--- a/system/src/Grav/Common/Data/BlueprintSchema.php
+++ b/system/src/Grav/Common/Data/BlueprintSchema.php
@@ -3,21 +3,33 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
+use Grav\Common\Config\Config;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\Blueprints\BlueprintSchema as BlueprintSchemaBase;
+use RuntimeException;
+use function is_array;
+use function is_string;
+/**
+ * Class BlueprintSchema
+ * @package Grav\Common\Data
+ */
class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
{
use Export;
+ /** @var array */
+ protected $filter = ['validation' => true, 'xss_check' => true];
+
+ /** @var array */
protected $ignoreFormKeys = [
'title' => true,
'help' => true,
@@ -44,18 +56,29 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
return $this->types[$name] ?? [];
}
+ /**
+ * @param string $name
+ * @return array|null
+ */
+ public function getNestedRules(string $name)
+ {
+ return $this->getNested($name);
+ }
+
/**
* Validate data against blueprints.
*
* @param array $data
- * @throws \RuntimeException
+ * @param array $options
+ * @return void
+ * @throws RuntimeException
*/
- public function validate(array $data)
+ public function validate(array $data, array $options = [])
{
try {
- $messages = $this->validateArray($data, $this->nested);
-
- } catch (\RuntimeException $e) {
+ $validation = $this->items['']['form']['validation'] ?? 'loose';
+ $messages = $this->validateArray($data, $this->nested, $validation === 'strict', $options['xss_check'] ?? true);
+ } catch (RuntimeException $e) {
throw (new ValidationException($e->getMessage(), $e->getCode(), $e))->setMessages();
}
@@ -71,7 +94,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
*/
public function processForm(array $data, array $toggles = [])
{
- return $this->processFormRecursive($data, $toggles, $this->nested);
+ return $this->processFormRecursive($data, $toggles, $this->nested) ?? [];
}
/**
@@ -84,18 +107,31 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
*/
public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false)
{
- return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues);
+ $this->buildIgnoreNested($this->nested);
+
+ return $this->filterArray($data, $this->nested, '', $missingValuesAsNull, $keepEmptyValues) ?? [];
}
/**
* Flatten data by using blueprints.
*
* @param array $data Data to be flattened.
+ * @param bool $includeAll
* @return array
*/
- public function flattenData(array $data)
+ public function flattenData(array $data, bool $includeAll = false)
{
- return $this->flattenArray($data, $this->nested, '');
+ $list = [];
+ if ($includeAll) {
+ foreach ($this->items as $key => $rules) {
+ $type = $rules['type'] ?? '';
+ if (!str_starts_with($type, '_') && !str_contains($key, '*')) {
+ $list[$key] = null;
+ }
+ }
+ }
+
+ return array_replace($list, $this->flattenArray($data, $this->nested, ''));
}
/**
@@ -123,22 +159,26 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
$array[$prefix.$key] = $field;
}
}
+
return $array;
}
/**
* @param array $data
* @param array $rules
+ * @param bool $strict
+ * @param bool $xss
* @return array
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
- protected function validateArray(array $data, array $rules)
+ protected function validateArray(array $data, array $rules, bool $strict, bool $xss = true)
{
$messages = $this->checkRequired($data, $rules);
foreach ($data as $key => $child) {
$val = $rules[$key] ?? $rules['*'] ?? null;
- $rule = \is_string($val) ? $this->items[$val] : null;
+ $rule = is_string($val) ? $this->items[$val] : null;
+ $checkXss = $xss;
if ($rule) {
// Item has been defined in blueprints.
@@ -148,12 +188,25 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
}
$messages += Validation::validate($child, $rule);
- } elseif (\is_array($child) && \is_array($val)) {
+
+ } elseif (is_array($child) && is_array($val)) {
// Array has been defined in blueprints.
- $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));
+ $messages += $this->validateArray($child, $val, $strict);
+ $checkXss = false;
+
+ } elseif ($strict) {
+ // Undefined/extra item in strict mode.
+ /** @var Config $config */
+ $config = Grav::instance()['config'];
+ if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) {
+ throw new RuntimeException(sprintf('%s is not defined in blueprints', $key));
+ }
+
+ user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED);
+ }
+
+ if ($checkXss) {
+ $messages += Validation::checkSafety($child, $rule ?: ['name' => $key]);
}
}
@@ -163,51 +216,55 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
/**
* @param array $data
* @param array $rules
+ * @param string $parent
* @param bool $missingValuesAsNull
* @param bool $keepEmptyValues
- * @return array
+ * @return array|null
*/
- protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues)
+ protected function filterArray(array $data, array $rules, string $parent, bool $missingValuesAsNull, bool $keepEmptyValues)
{
$results = [];
- if ($missingValuesAsNull) {
- // First pass is to fill up all the fields with null. This is done to lock the ordering of the fields.
- foreach ($rules as $key => $rule) {
- if ($key && !isset($results[$key])) {
- $val = $rules[$key] ?? $rules['*'] ?? null;
- $rule = \is_string($val) ? $this->items[$val] : null;
-
- if (empty($rule['disabled']) && empty($rule['validate']['ignore'])) {
- continue;
- }
- }
- }
- }
-
foreach ($data as $key => $field) {
$val = $rules[$key] ?? $rules['*'] ?? null;
- $rule = \is_string($val) ? $this->items[$val] : null;
+ $rule = is_string($val) ? $this->items[$val] : $this->items[$parent . $key] ?? null;
- if ($rule) {
- // Item has been defined in blueprints.
- if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
- // Skip any data in the ignored field.
+ if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
+ // Skip any data in the ignored field.
+ unset($results[$key]);
+ continue;
+ }
+
+ if (null === $field) {
+ if ($missingValuesAsNull) {
+ $results[$key] = null;
+ } else {
unset($results[$key]);
- continue;
}
+ continue;
+ }
+
+ $isParent = isset($val['*']);
+ $type = $rule['type'] ?? null;
+ if (!$isParent && $type && $type !== '_parent') {
$field = Validation::filter($field, $rule);
- } elseif (\is_array($field) && \is_array($val)) {
+ } elseif (is_array($field) && is_array($val)) {
// Array has been defined in blueprints.
- $field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues);
+ $k = $isParent ? '*' : $key;
+ $field = $this->filterArray($field, $val, $parent . $k . '.', $missingValuesAsNull, $keepEmptyValues);
+ if (null === $field) {
+ // Nested parent has no values.
+ unset($results[$key]);
+ continue;
+ }
} elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
// Skip any extra data.
continue;
}
- if ($keepEmptyValues || (null !== $field && (!\is_array($field) || !empty($field)))) {
+ if ($keepEmptyValues || (null !== $field && (!is_array($field) || !empty($field)))) {
$results[$key] = $field;
}
}
@@ -215,6 +272,31 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
return $results ?: null;
}
+ /**
+ * @param array $nested
+ * @param string $parent
+ * @return bool
+ */
+ protected function buildIgnoreNested(array $nested, $parent = '')
+ {
+ $ignore = true;
+ foreach ($nested as $key => $val) {
+ $key = $parent . $key;
+ if (is_array($val)) {
+ $ignore = $this->buildIgnoreNested($val, $key . '.') && $ignore; // Keep the order!
+ } else {
+ $child = $this->items[$key] ?? null;
+ $ignore = $ignore && (!$child || !empty($child['disabled']) || !empty($child['validate']['ignore']));
+ }
+ }
+ if ($ignore) {
+ $key = trim($parent, '.');
+ $this->items[$key]['validate']['ignore'] = true;
+ }
+
+ return $ignore;
+ }
+
/**
* @param array|null $data
* @param array $toggles
@@ -232,8 +314,23 @@ 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);
+ $childData = $data[$key] ?? null;
+ if (null !== $childData && !is_array($childData)) {
+ throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
+ }
+ $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
} else {
$field = $this->get($value);
// Do not add the field if:
@@ -244,8 +341,8 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|| !empty($field['disabled'])
// Field validation is set to be ignored
|| !empty($field['validate']['ignore'])
- // Field is toggleable and the toggle is turned off
- || (!empty($field['toggleable']) && empty($toggles[$key]))
+ // Field is overridable and the toggle is turned off
+ || (!empty($field['overridable']) && empty($toggles[$key]))
) {
continue;
}
@@ -268,7 +365,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
$messages = [];
foreach ($fields as $name => $field) {
- if (!\is_string($field)) {
+ if (!is_string($field)) {
continue;
}
@@ -279,10 +376,15 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
continue;
}
+ // Skip overridable fields without value.
+ // TODO: We need better overridable support, which is not just ignoring required values but also looking if defaults are good.
+ if (!empty($field['overridable']) && !isset($data[$name])) {
+ continue;
+ }
+
// Check if required.
if (isset($field['validate']['required'])
&& $field['validate']['required'] === true) {
-
if (isset($data[$name])) {
continue;
}
@@ -304,6 +406,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
* @param array $field
* @param string $property
* @param array $call
+ * @return void
*/
protected function dynamicConfig(array &$field, $property, array &$call)
{
diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php
index 1d0fcc68..abe153f1 100644
--- a/system/src/Grav/Common/Data/Blueprints.php
+++ b/system/src/Grav/Common/Data/Blueprints.php
@@ -3,15 +3,23 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
+use DirectoryIterator;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use function is_array;
+use function is_object;
+/**
+ * Class Blueprints
+ * @package Grav\Common\Data
+ */
class Blueprints
{
/** @var array|string */
@@ -34,7 +42,7 @@ class Blueprints
*
* @param string $type Blueprint type.
* @return Blueprint
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public function get($type)
{
@@ -65,10 +73,9 @@ class Blueprints
if ($locator->isStream($this->search)) {
$iterator = $locator->getIterator($this->search);
} else {
- $iterator = new \DirectoryIterator($this->search);
+ $iterator = new DirectoryIterator($this->search);
}
- /** @var \DirectoryIterator $file */
foreach ($iterator as $file) {
if (!$file->isFile() || '.' . $file->getExtension() !== YAML_EXT) {
continue;
@@ -92,7 +99,7 @@ class Blueprints
{
$blueprint = new Blueprint($name);
- if (\is_array($this->search) || \is_object($this->search)) {
+ if (is_array($this->search) || is_object($this->search)) {
// Page types.
$blueprint->setOverrides($this->search);
$blueprint->setContext('blueprints://pages');
@@ -102,7 +109,7 @@ class Blueprints
try {
$blueprint->load()->init();
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
$log = Grav::instance()['log'];
$log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage()));
diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php
index 97b55308..4de437dc 100644
--- a/system/src/Grav/Common/Data/Data.php
+++ b/system/src/Grav/Common/Data/Data.php
@@ -3,49 +3,58 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
+use ArrayAccess;
+use Exception;
+use JsonSerializable;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
-use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\File\FileInterface;
+use RuntimeException;
+use function func_get_args;
+use function is_array;
+use function is_callable;
+use function is_object;
-class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable, ExportInterface
+/**
+ * Class Data
+ * @package Grav\Common\Data
+ */
+class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable, ExportInterface
{
use NestedArrayAccessWithGetters, Countable, Export;
/** @var string */
protected $gettersVariable = 'items';
-
/** @var array */
protected $items;
-
- /** @var Blueprint */
+ /** @var Blueprint|callable|null */
protected $blueprints;
-
- /** @var File */
+ /** @var FileInterface|null */
protected $storage;
/** @var bool */
private $missingValuesAsNull = false;
-
/** @var bool */
private $keepEmptyValues = true;
/**
* @param array $items
- * @param Blueprint|callable $blueprints
+ * @param Blueprint|callable|null $blueprints
*/
public function __construct(array $items = [], $blueprints = null)
{
$this->items = $items;
- $this->blueprints = $blueprints;
+ if (null !== $blueprints) {
+ $this->blueprints = $blueprints;
+ }
}
/**
@@ -92,20 +101,20 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
* @param mixed $value Value to be joined.
* @param string $separator Separator, defaults to '.'
* @return $this
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public function join($name, $value, $separator = '.')
{
$old = $this->get($name, null, $separator);
if ($old !== null) {
- if (!\is_array($old)) {
- throw new \RuntimeException('Value ' . $old);
+ if (!is_array($old)) {
+ throw new RuntimeException('Value ' . $old);
}
- if (\is_object($value)) {
+ if (is_object($value)) {
$value = (array) $value;
- } elseif (!\is_array($value)) {
- throw new \RuntimeException('Value ' . $value);
+ } elseif (!is_array($value)) {
+ throw new RuntimeException('Value ' . $value);
}
$value = $this->blueprints()->mergeData($old, $value, $name, $separator);
@@ -138,7 +147,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
*/
public function joinDefaults($name, $value, $separator = '.')
{
- if (\is_object($value)) {
+ if (is_object($value)) {
$value = (array) $value;
}
@@ -159,14 +168,14 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
* @param array|object $value Value to be joined.
* @param string $separator Separator, defaults to '.'
* @return array
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public function getJoined($name, $value, $separator = '.')
{
- if (\is_object($value)) {
+ if (is_object($value)) {
$value = (array) $value;
- } elseif (!\is_array($value)) {
- throw new \RuntimeException('Value ' . $value);
+ } elseif (!is_array($value)) {
+ throw new RuntimeException('Value ' . $value);
}
$old = $this->get($name, null, $separator);
@@ -176,8 +185,8 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
return $value;
}
- if (!\is_array($old)) {
- throw new \RuntimeException('Value ' . $old);
+ if (!is_array($old)) {
+ throw new RuntimeException('Value ' . $old);
}
// Return joined data.
@@ -215,7 +224,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
* Validate by blueprints.
*
* @return $this
- * @throws \Exception
+ * @throws Exception
*/
public function validate()
{
@@ -255,19 +264,22 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
*/
public function blueprints()
{
- if (!$this->blueprints){
- $this->blueprints = new Blueprint;
- } elseif (\is_callable($this->blueprints)) {
+ if (null === $this->blueprints) {
+ $this->blueprints = new Blueprint();
+ } elseif (is_callable($this->blueprints)) {
// Lazy load blueprints.
$blueprints = $this->blueprints;
$this->blueprints = $blueprints();
}
+
return $this->blueprints;
}
/**
* Save data if storage has been defined.
- * @throws \RuntimeException
+ *
+ * @return void
+ * @throws RuntimeException
*/
public function save()
{
@@ -308,8 +320,8 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
/**
* Set or get the data storage.
*
- * @param FileInterface $storage Optionally enter a new storage.
- * @return FileInterface
+ * @param FileInterface|null $storage Optionally enter a new storage.
+ * @return FileInterface|null
*/
public function file(FileInterface $storage = null)
{
@@ -320,6 +332,9 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable
return $this->storage;
}
+ /**
+ * @return array
+ */
public function jsonSerialize()
{
return $this->items;
diff --git a/system/src/Grav/Common/Data/DataInterface.php b/system/src/Grav/Common/Data/DataInterface.php
index 91279b28..b4452bb8 100644
--- a/system/src/Grav/Common/Data/DataInterface.php
+++ b/system/src/Grav/Common/Data/DataInterface.php
@@ -3,14 +3,19 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
+use Exception;
use RocketTheme\Toolbox\File\FileInterface;
+/**
+ * Interface DataInterface
+ * @package Grav\Common\Data
+ */
interface DataInterface
{
/**
@@ -35,35 +40,44 @@ interface DataInterface
/**
* Return blueprints.
+ *
+ * @return Blueprint
*/
public function blueprints();
/**
* Validate by blueprints.
*
- * @throws \Exception
+ * @return $this
+ * @throws Exception
*/
public function validate();
/**
* Filter all items by using blueprints.
+ *
+ * @return $this
*/
public function filter();
/**
* Get extra items which haven't been defined in blueprints.
+ *
+ * @return array
*/
public function extra();
/**
* Save data into the file.
+ *
+ * @return void
*/
public function save();
/**
* Set or get the data storage.
*
- * @param FileInterface $storage Optionally enter a new storage.
+ * @param FileInterface|null $storage Optionally enter a new storage.
* @return FileInterface
*/
public function file(FileInterface $storage = null);
diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php
index a2bf0c5c..c5b21ffa 100644
--- a/system/src/Grav/Common/Data/Validation.php
+++ b/system/src/Grav/Common/Data/Validation.php
@@ -3,16 +3,35 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
+use ArrayAccess;
+use Countable;
+use DateTime;
+use Grav\Common\Config\Config;
use Grav\Common\Grav;
+use Grav\Common\Language\Language;
+use Grav\Common\Security;
+use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Common\Yaml;
+use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
+use Traversable;
+use function count;
+use function is_array;
+use function is_bool;
+use function is_float;
+use function is_int;
+use function is_string;
+/**
+ * Class Validation
+ * @package Grav\Common\Data
+ */
class Validation
{
/**
@@ -44,7 +63,7 @@ class Validation
$name = ucfirst($field['label'] ?? $field['name']);
$message = (string) isset($field['validate']['message'])
? $language->translate($field['validate']['message'])
- : $language->translate('GRAV.FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
+ : $language->translate('GRAV.FORM.INVALID_INPUT') . ' "' . $language->translate($name) . '"';
// Validate type with fallback type text.
@@ -78,6 +97,92 @@ class Validation
return $messages;
}
+ /**
+ * @param mixed $value
+ * @param array $field
+ * @return array
+ */
+ public static function checkSafety($value, array $field)
+ {
+ $messages = [];
+
+ $type = $field['validate']['type'] ?? $field['type'] ?? 'text';
+ $options = $field['xss_check'] ?? [];
+ if ($options === false || $type === 'unset') {
+ return $messages;
+ }
+ if (!is_array($options)) {
+ $options = [];
+ }
+
+ $name = ucfirst($field['label'] ?? $field['name'] ?? 'UNKNOWN');
+
+ /** @var UserInterface $user */
+ $user = Grav::instance()['user'] ?? null;
+ /** @var Config $config */
+ $config = Grav::instance()['config'];
+
+ $xss_whitelist = $config->get('security.xss_whitelist', 'admin.super');
+
+ // Get language class.
+ /** @var Language $language */
+ $language = Grav::instance()['language'];
+
+ if (!static::authorize($xss_whitelist, $user)) {
+ $defaults = Security::getXssDefaults();
+ $options += $defaults;
+ $options['enabled_rules'] += $defaults['enabled_rules'];
+ if (!empty($options['safe_protocols'])) {
+ $options['invalid_protocols'] = array_diff($options['invalid_protocols'], $options['safe_protocols']);
+ }
+ if (!empty($options['safe_tags'])) {
+ $options['dangerous_tags'] = array_diff($options['dangerous_tags'], $options['safe_tags']);
+ }
+
+ if (is_string($value)) {
+ $violation = Security::detectXss($value, $options);
+ if ($violation) {
+ $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
+ }
+ } elseif (is_array($value)) {
+ $violations = Security::detectXssFromArray($value, "{$name}.", $options);
+ if ($violations) {
+ $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
+ }
+ }
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Checks user authorisation to the action.
+ *
+ * @param string|string[] $action
+ * @param UserInterface|null $user
+ * @return bool
+ */
+ public static function authorize($action, UserInterface $user = null)
+ {
+ if (!$user) {
+ return false;
+ }
+
+ $action = (array)$action;
+ foreach ($action as $a) {
+ // Ignore 'admin.super' if it's not the only value to be checked.
+ if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
+ continue;
+ }
+
+ if ($user->authorize($a)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Filter value against a blueprint field definition.
*
@@ -123,7 +228,7 @@ class Validation
*/
public static function typeText($value, array $params, array $field)
{
- if (!\is_string($value) && !is_numeric($value)) {
+ if (!is_string($value) && !is_numeric($value)) {
return false;
}
@@ -133,16 +238,21 @@ class Validation
$value = trim($value);
}
- if (isset($params['min']) && \strlen($value) < $params['min']) {
+ $value = preg_replace("/\r\n|\r/um", "\n", $value);
+ $len = mb_strlen($value);
+
+ $min = (int)($params['min'] ?? 0);
+ if ($min && $len < $min) {
return false;
}
- if (isset($params['max']) && \strlen($value) > $params['max']) {
+ $max = (int)($params['max'] ?? 0);
+ if ($max && $len > $max) {
return false;
}
- $min = $params['min'] ?? 0;
- if (isset($params['step']) && (\strlen($value) - $min) % $params['step'] === 0) {
+ $step = (int)($params['step'] ?? 0);
+ if ($step && ($len - $min) % $step === 0) {
return false;
}
@@ -153,17 +263,25 @@ class Validation
return true;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return string
+ */
protected static function filterText($value, array $params, array $field)
{
- if (!\is_string($value) && !is_numeric($value)) {
+ if (!is_string($value) && !is_numeric($value)) {
return '';
}
+ $value = (string)$value;
+
if (!empty($params['trim'])) {
$value = trim($value);
}
- return (string) $value;
+ return preg_replace("/\r\n|\r/um", "\n", $value);
}
/**
@@ -180,24 +298,57 @@ class Validation
return $value === $field_value ? $value : null;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array|array[]|false|string[]
+ */
protected static function filterCommaList($value, array $params, array $field)
{
- return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return bool
+ */
public static function typeCommaList($value, array $params, array $field)
{
- return \is_array($value) ? true : self::typeText($value, $params, $field);
+ return is_array($value) ? true : self::typeText($value, $params, $field);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array|array[]|false|string[]
+ */
+ protected static function filterLines($value, array $params, array $field)
+ {
+ return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @return string
+ */
protected static function filterLower($value, array $params)
{
- return strtolower($value);
+ return mb_strtolower($value);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @return string
+ */
protected static function filterUpper($value, array $params)
{
- return strtoupper($value);
+ return mb_strtoupper($value);
}
@@ -260,6 +411,12 @@ class Validation
return self::typeArray((array) $value, $params, $field);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array|null
+ */
protected static function filterCheckboxes($value, array $params, array $field)
{
return self::filterArray($value, $params, $field);
@@ -304,7 +461,7 @@ class Validation
*/
public static function typeToggle($value, array $params, array $field)
{
- if (\is_bool($value)) {
+ if (is_bool($value)) {
$value = (int)$value;
}
@@ -324,6 +481,12 @@ class Validation
return self::typeArray((array)$value, $params, $field);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array
+ */
protected static function filterFile($value, array $params, array $field)
{
return (array)$value;
@@ -356,35 +519,61 @@ class Validation
return false;
}
- if (isset($params['min']) && $value < $params['min']) {
- return false;
+ $value = (float)$value;
+
+ $min = 0;
+ if (isset($params['min'])) {
+ $min = (float)$params['min'];
+ if ($value < $min) {
+ return false;
+ }
}
- if (isset($params['max']) && $value > $params['max']) {
- return false;
+ if (isset($params['max'])) {
+ $max = (float)$params['max'];
+ if ($value > $max) {
+ return false;
+ }
}
- $min = $params['min'] ?? 0;
+ if (isset($params['step'])) {
+ $step = (float)$params['step'];
+ // Count of how many steps we are above/below the minimum value.
+ $pos = ($value - $min) / $step;
+
+ return is_int(static::filterNumber($pos, $params, $field));
+ }
- return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
+ return true;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return float|int
+ */
protected static function filterNumber($value, array $params, array $field)
{
- return (string)(int)$value !== (string)(float)$value ? (float) $value : (int) $value;
+ return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return string
+ */
protected static function filterDateTime($value, array $params, array $field)
{
$format = Grav::instance()['config']->get('system.pages.dateformat.default');
if ($format) {
- $converted = new \DateTime($value);
+ $converted = new DateTime($value);
return $converted->format($format);
}
return $value;
}
-
/**
* HTML5 input: range
*
@@ -398,6 +587,12 @@ class Validation
return self::typeNumber($value, $params, $field);
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return float|int
+ */
protected static function filterRange($value, array $params, array $field)
{
return self::filterNumber($value, $params, $field);
@@ -413,7 +608,7 @@ class Validation
*/
public static function typeColor($value, array $params, array $field)
{
- return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
+ return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
}
/**
@@ -426,7 +621,7 @@ class Validation
*/
public static function typeEmail($value, array $params, array $field)
{
- $values = !\is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
+ $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
foreach ($values as $val) {
if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
@@ -445,7 +640,6 @@ class Validation
* @param array $field Blueprint for the field.
* @return bool True if validation succeeded.
*/
-
public static function typeUrl($value, array $params, array $field)
{
return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL);
@@ -461,17 +655,17 @@ class Validation
*/
public static function typeDatetime($value, array $params, array $field)
{
- if ($value instanceof \DateTime) {
+ if ($value instanceof DateTime) {
return true;
}
- if (!\is_string($value)) {
+ if (!is_string($value)) {
return false;
}
if (!isset($params['format'])) {
return false !== strtotime($value);
}
- $dateFromFormat = \DateTime::createFromFormat($params['format'], $value);
+ $dateFromFormat = DateTime::createFromFormat($params['format'], $value);
return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp());
}
@@ -567,21 +761,21 @@ class Validation
*/
public static function typeArray($value, array $params, array $field)
{
- if (!\is_array($value)) {
+ if (!is_array($value)) {
return false;
}
if (isset($field['multiple'])) {
- if (isset($params['min']) && \count($value) < $params['min']) {
+ if (isset($params['min']) && count($value) < $params['min']) {
return false;
}
- if (isset($params['max']) && \count($value) > $params['max']) {
+ if (isset($params['max']) && count($value) > $params['max']) {
return false;
}
$min = $params['min'] ?? 0;
- if (isset($params['step']) && (\count($value) - $min) % $params['step'] === 0) {
+ if (isset($params['step']) && (count($value) - $min) % $params['step'] === 0) {
return false;
}
}
@@ -604,13 +798,32 @@ class Validation
return !($options && array_diff($value, $options));
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array|null
+ */
+ protected static function filterFlatten_array($value, $params, $field)
+ {
+ $value = static::filterArray($value, $params, $field);
+
+ return Utils::arrayUnflattenDotNotation($value);
+ }
+
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array|null
+ */
protected static function filterArray($value, $params, $field)
{
$values = (array) $value;
$options = isset($field['options']) ? array_keys($field['options']) : [];
$multi = $field['multiple'] ?? false;
- if (\count($values) === 1 && isset($values[0]) && $values[0] === '') {
+ if (count($values) === 1 && isset($values[0]) && $values[0] === '') {
return null;
}
@@ -624,7 +837,7 @@ class Validation
if ($multi) {
foreach ($values as $key => $val) {
- if (\is_array($val)) {
+ if (is_array($val)) {
$val = implode(',', $val);
$values[$key] = array_map('trim', explode(',', $val));
} else {
@@ -633,28 +846,90 @@ class Validation
}
}
- if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) {
- foreach ($values as $key => $val) {
- if ($val === '') {
+ $ignoreEmpty = isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty']);
+ $valueType = $params['value_type'] ?? null;
+ $keyType = $params['key_type'] ?? null;
+ if ($ignoreEmpty || $valueType || $keyType) {
+ $values = static::arrayFilterRecurse($values, ['value_type' => $valueType, 'key_type' => $keyType, 'ignore_empty' => $ignoreEmpty]);
+ }
+
+ return $values;
+ }
+
+ /**
+ * @param array $values
+ * @param array $params
+ * @return array
+ */
+ protected static function arrayFilterRecurse(array $values, array $params): array
+ {
+ foreach ($values as $key => &$val) {
+ if ($params['key_type']) {
+ switch ($params['key_type']) {
+ case 'int':
+ $result = is_int($key);
+ break;
+ case 'string':
+ $result = is_string($key);
+ break;
+ default:
+ $result = false;
+ }
+ if (!$result) {
unset($values[$key]);
- } elseif (\is_array($val)) {
- foreach ($val as $inner_key => $inner_value) {
- if ($inner_value === '') {
- unset($val[$inner_key]);
- }
+ }
+ }
+ if (is_array($val)) {
+ $val = static::arrayFilterRecurse($val, $params);
+ if ($params['ignore_empty'] && empty($val)) {
+ unset($values[$key]);
+ }
+ } else {
+ if ($params['value_type'] && $val !== '' && $val !== null) {
+ switch ($params['value_type']) {
+ case 'bool':
+ if (Utils::isPositive($val)) {
+ $val = true;
+ } elseif (Utils::isNegative($val)) {
+ $val = false;
+ } else {
+ // Ignore invalid bool values.
+ $val = null;
+ }
+ break;
+ case 'int':
+ $val = (int)$val;
+ break;
+ case 'float':
+ $val = (float)$val;
+ break;
+ case 'string':
+ $val = (string)$val;
+ break;
+ case 'trim':
+ $val = trim($val);
+ break;
}
}
- $values[$key] = $val;
+ if ($params['ignore_empty'] && ($val === '' || $val === null)) {
+ unset($values[$key]);
+ }
}
}
return $values;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return bool
+ */
public static function typeList($value, array $params, array $field)
{
- if (!\is_array($value)) {
+ if (!is_array($value)) {
return false;
}
@@ -671,19 +946,29 @@ class Validation
return true;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return array
+ */
protected static function filterList($value, array $params, array $field)
{
return (array) $value;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @return array
+ */
public static function filterYaml($value, $params)
{
- if (!\is_string($value)) {
+ if (!is_string($value)) {
return $value;
}
return (array) Yaml::parse($value);
-
}
/**
@@ -699,6 +984,12 @@ class Validation
return true;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return mixed
+ */
public static function filterIgnore($value, array $params, array $field)
{
return $value;
@@ -717,6 +1008,12 @@ class Validation
return true;
}
+ /**
+ * @param mixed $value
+ * @param array $params
+ * @param array $field
+ * @return null
+ */
public static function filterUnset($value, array $params, array $field)
{
return null;
@@ -724,6 +1021,11 @@ class Validation
// HTML5 attributes (min, max and range are handled inside the types)
+ /**
+ * @param mixed $value
+ * @param bool $params
+ * @return bool
+ */
public static function validateRequired($value, $params)
{
if (is_scalar($value)) {
@@ -733,79 +1035,170 @@ class Validation
return (bool) $params !== true || !empty($value);
}
+ /**
+ * @param mixed $value
+ * @param string $params
+ * @return bool
+ */
public static function validatePattern($value, $params)
{
return (bool) preg_match("`^{$params}$`u", $value);
}
-
// Internal types
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateAlpha($value, $params)
{
return ctype_alpha($value);
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateAlnum($value, $params)
{
return ctype_alnum($value);
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function typeBool($value, $params)
{
- return \is_bool($value) || $value == 1 || $value == 0;
+ return is_bool($value) || $value == 1 || $value == 0;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateBool($value, $params)
{
- return \is_bool($value) || $value == 1 || $value == 0;
+ return is_bool($value) || $value == 1 || $value == 0;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
protected static function filterBool($value, $params)
{
return (bool) $value;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateDigit($value, $params)
{
return ctype_digit($value);
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateFloat($value, $params)
{
- return \is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
+ return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return float
+ */
protected static function filterFloat($value, $params)
{
return (float) $value;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateHex($value, $params)
{
return ctype_xdigit($value);
}
+ /**
+ * Custom input: int
+ *
+ * @param mixed $value Value to be validated.
+ * @param array $params Validation parameters.
+ * @param array $field Blueprint for the field.
+ * @return bool True if validation succeeded.
+ */
+ public static function typeInt($value, array $params, array $field)
+ {
+ $params['step'] = max(1, (int)($params['step'] ?? 0));
+
+ return self::typeNumber($value, $params, $field);
+ }
+
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateInt($value, $params)
{
return is_numeric($value) && (int)$value == $value;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return int
+ */
protected static function filterInt($value, $params)
{
return (int)$value;
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateArray($value, $params)
{
- return \is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable && $value instanceof \Countable);
+ return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable);
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return array
+ */
public static function filterItem_List($value, $params)
{
- return array_values(array_filter($value, function($v) { return !empty($v); } ));
+ return array_values(array_filter($value, static function ($v) {
+ return !empty($v);
+ }));
}
+ /**
+ * @param mixed $value
+ * @param mixed $params
+ * @return bool
+ */
public static function validateJson($value, $params)
{
return (bool) (@json_decode($value));
diff --git a/system/src/Grav/Common/Data/ValidationException.php b/system/src/Grav/Common/Data/ValidationException.php
index 8bdb0572..2d94ab81 100644
--- a/system/src/Grav/Common/Data/ValidationException.php
+++ b/system/src/Grav/Common/Data/ValidationException.php
@@ -3,19 +3,30 @@
/**
* @package Grav\Common\Data
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Data;
use Grav\Common\Grav;
+use RuntimeException;
-class ValidationException extends \RuntimeException
+/**
+ * Class ValidationException
+ * @package Grav\Common\Data
+ */
+class ValidationException extends RuntimeException
{
+ /** @var array */
protected $messages = [];
- public function setMessages(array $messages = []) {
+ /**
+ * @param array $messages
+ * @return $this
+ */
+ public function setMessages(array $messages = [])
+ {
$this->messages = $messages;
$language = Grav::instance()['language'];
@@ -31,6 +42,9 @@ class ValidationException extends \RuntimeException
return $this;
}
+ /**
+ * @return array
+ */
public function getMessages()
{
return $this->messages;
diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php
index 07372094..b8143f68 100644
--- a/system/src/Grav/Common/Debugger.php
+++ b/system/src/Grav/Common/Debugger.php
@@ -3,12 +3,19 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use Clockwork\Clockwork;
+use Clockwork\DataSource\MonologDataSource;
+use Clockwork\DataSource\PsrMessageDataSource;
+use Clockwork\DataSource\XdebugDataSource;
+use Clockwork\Helpers\ServerTiming;
+use Clockwork\Request\UserData;
+use Clockwork\Storage\FileStorage;
use DebugBar\DataCollector\ConfigCollector;
use DebugBar\DataCollector\DataCollectorInterface;
use DebugBar\DataCollector\ExceptionsCollector;
@@ -18,76 +25,106 @@ use DebugBar\DataCollector\PhpInfoCollector;
use DebugBar\DataCollector\RequestDataCollector;
use DebugBar\DataCollector\TimeDataCollector;
use DebugBar\DebugBar;
+use DebugBar\DebugBarException;
use DebugBar\JavascriptRenderer;
-use DebugBar\StandardDebugBar;
use Grav\Common\Config\Config;
use Grav\Common\Processors\ProcessorInterface;
+use Grav\Common\Twig\TwigClockworkDataSource;
+use Grav\Framework\Psr7\Response;
+use Monolog\Logger;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use ReflectionObject;
+use SplFileInfo;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Throwable;
+use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
+use function array_slice;
+use function call_user_func;
+use function count;
+use function define;
+use function defined;
+use function extension_loaded;
+use function get_class;
+use function gettype;
+use function is_array;
+use function is_bool;
+use function is_object;
+use function is_scalar;
+use function is_string;
+/**
+ * Class Debugger
+ * @package Grav\Common
+ */
class Debugger
{
- /** @var Grav $grav */
+ /** @var static */
+ protected static $instance;
+ /** @var Grav|null */
protected $grav;
-
- /** @var Config $config */
+ /** @var Config|null */
protected $config;
-
- /** @var JavascriptRenderer $renderer */
+ /** @var JavascriptRenderer|null */
protected $renderer;
-
- /** @var StandardDebugBar $debugbar */
+ /** @var DebugBar|null */
protected $debugbar;
-
+ /** @var Clockwork|null */
+ protected $clockwork;
+ /** @var bool */
+ protected $enabled = false;
/** @var bool */
- protected $enabled;
-
protected $initialized = false;
-
/** @var array */
protected $timers = [];
-
- /** @var array $deprecations */
+ /** @var array */
protected $deprecations = [];
-
- /** @var callable */
+ /** @var callable|null */
protected $errorHandler;
+ /** @var float */
+ protected $requestTime;
+ /** @var float */
+ protected $currentTime;
+ /** @var int */
+ protected $profiling = 0;
+ /** @var bool */
+ protected $censored = false;
/**
* Debugger constructor.
*/
public function __construct()
{
- $currentTime = microtime(true);
-
- if (!\defined('GRAV_REQUEST_TIME')) {
- \define('GRAV_REQUEST_TIME', $currentTime);
- }
-
- // Enable debugger until $this->init() gets called.
- $this->enabled = true;
+ static::$instance = $this;
- $debugbar = new DebugBar();
- $debugbar->addCollector(new PhpInfoCollector());
- $debugbar->addCollector(new MessagesCollector());
- $debugbar->addCollector(new RequestDataCollector());
- $debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME));
+ $this->currentTime = microtime(true);
- $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
- $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime);
- $debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true));
+ if (!defined('GRAV_REQUEST_TIME')) {
+ define('GRAV_REQUEST_TIME', $this->currentTime);
+ }
- $this->debugbar = $debugbar;
+ $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
// Set deprecation collector.
$this->setErrorHandler();
}
+ /**
+ * @return Clockwork|null
+ */
+ public function getClockwork(): ?Clockwork
+ {
+ return $this->enabled ? $this->clockwork : null;
+ }
+
/**
* Initialize the debugger
*
* @return $this
- * @throws \DebugBar\DebugBarException
+ * @throws DebugBarException
*/
public function init()
{
@@ -100,30 +137,242 @@ class Debugger
// Enable/disable debugger based on configuration.
$this->enabled = (bool)$this->config->get('system.debugger.enabled');
+ $this->censored = (bool)$this->config->get('system.debugger.censored', false);
- if ($this->enabled()) {
+ if ($this->enabled) {
$this->initialized = true;
- $plugins_config = (array)$this->config->get('plugins');
+ $clockwork = $debugbar = null;
+ switch ($this->config->get('system.debugger.provider', 'debugbar')) {
+ case 'clockwork':
+ $this->clockwork = $clockwork = new Clockwork();
+ break;
+ default:
+ $this->debugbar = $debugbar = new DebugBar();
+ }
+
+ $plugins_config = (array)$this->config->get('plugins');
ksort($plugins_config);
- $debugbar = $this->debugbar;
- $debugbar->addCollector(new MemoryCollector());
- $debugbar->addCollector(new ExceptionsCollector());
- $debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
- $debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
- $this->addMessage('Grav v' . GRAV_VERSION);
+ if ($clockwork) {
+ $log = $this->grav['log'];
+ $clockwork->setStorage(new FileStorage('cache://clockwork'));
+ if (extension_loaded('xdebug')) {
+ $clockwork->addDataSource(new XdebugDataSource());
+ }
+ if ($log instanceof Logger) {
+ $clockwork->addDataSource(new MonologDataSource($log));
+ }
+
+ $timeline = $clockwork->timeline();
+ if ($this->requestTime !== GRAV_REQUEST_TIME) {
+ $event = $timeline->event('Server');
+ $event->finalize($this->requestTime, GRAV_REQUEST_TIME);
+ }
+ if ($this->currentTime !== GRAV_REQUEST_TIME) {
+ $event = $timeline->event('Loading');
+ $event->finalize(GRAV_REQUEST_TIME, $this->currentTime);
+ }
+ $event = $timeline->event('Site Setup');
+ $event->finalize($this->currentTime, microtime(true));
+ }
+
+ if ($this->censored) {
+ $censored = ['CENSORED' => true];
+ }
+
+ if ($debugbar) {
+ $debugbar->addCollector(new PhpInfoCollector());
+ $debugbar->addCollector(new MessagesCollector());
+ if (!$this->censored) {
+ $debugbar->addCollector(new RequestDataCollector());
+ }
+ $debugbar->addCollector(new TimeDataCollector($this->requestTime));
+ $debugbar->addCollector(new MemoryCollector());
+ $debugbar->addCollector(new ExceptionsCollector());
+ $debugbar->addCollector(new ConfigCollector($censored ?? (array)$this->config->get('system'), 'Config'));
+ $debugbar->addCollector(new ConfigCollector($censored ?? $plugins_config, 'Plugins'));
+ $debugbar->addCollector(new ConfigCollector($this->config->get('streams.schemes'), 'Streams'));
+
+ if ($this->requestTime !== GRAV_REQUEST_TIME) {
+ $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
+ }
+ if ($this->currentTime !== GRAV_REQUEST_TIME) {
+ $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $this->currentTime);
+ }
+ $debugbar['time']->addMeasure('Site Setup', $this->currentTime, microtime(true));
+ }
+
+ $this->addMessage('Grav v' . GRAV_VERSION . ' - PHP ' . PHP_VERSION);
+ $this->config->debug();
+
+ if ($clockwork) {
+ $clockwork->info('System Configuration', $censored ?? $this->config->get('system'));
+ $clockwork->info('Plugins Configuration', $censored ?? $plugins_config);
+ $clockwork->info('Streams', $this->config->get('streams.schemes'));
+ }
}
return $this;
}
+ public function finalize(): void
+ {
+ if ($this->clockwork && $this->enabled) {
+ $this->stopProfiling('Profiler Analysis');
+ $this->addMeasures();
+
+ $deprecations = $this->getDeprecations();
+ $count = count($deprecations);
+ if (!$count) {
+ return;
+ }
+
+ /** @var UserData $userData */
+ $userData = $this->clockwork->userData('Deprecated');
+ $userData->counters([
+ 'Deprecated' => count($deprecations)
+ ]);
+ /*
+ foreach ($deprecations as &$deprecation) {
+ $d = $deprecation;
+ unset($d['message']);
+ $this->clockwork->log('deprecated', $deprecation['message'], $d);
+ }
+ unset($deprecation);
+ */
+
+ $userData->table('Your site is using following deprecated features', $deprecations);
+ }
+ }
+
+ public function logRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+ {
+ if (!$this->enabled || !$this->clockwork) {
+ return $response;
+ }
+
+ $clockwork = $this->clockwork;
+
+ $this->finalize();
+
+ $clockwork->timeline()->finalize($request->getAttribute('request_time'));
+
+ if ($this->censored) {
+ $censored = 'CENSORED';
+ $request = $request
+ ->withCookieParams([$censored => ''])
+ ->withUploadedFiles([])
+ ->withHeader('cookie', $censored);
+ $request = $request->withParsedBody([$censored => '']);
+ }
+
+ $clockwork->addDataSource(new PsrMessageDataSource($request, $response));
+
+ $clockwork->resolveRequest();
+ $clockwork->storeRequest();
+
+ $clockworkRequest = $clockwork->getRequest();
+
+ $response = $response
+ ->withHeader('X-Clockwork-Id', $clockworkRequest->id)
+ ->withHeader('X-Clockwork-Version', $clockwork::VERSION);
+
+ $grav = Grav::instance();
+ $basePath = $this->grav['base_url_relative'] . $grav['pages']->base();
+ if ($basePath) {
+ $response = $response->withHeader('X-Clockwork-Path', $basePath . '/__clockwork/');
+ }
+
+ return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value());
+ }
+
+
+ public function debuggerRequest(RequestInterface $request): Response
+ {
+ $clockwork = $this->clockwork;
+
+ $headers = [
+ 'Content-Type' => 'application/json',
+ 'Grav-Internal-SkipShutdown' => 1
+ ];
+
+ $path = $request->getUri()->getPath();
+ $clockworkDataUri = '#/__clockwork(?:/(?[0-9-]+))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#';
+ if (preg_match($clockworkDataUri, $path, $matches) === false) {
+ $response = ['message' => 'Bad Input'];
+
+ return new Response(400, $headers, json_encode($response));
+ }
+
+ $id = $matches['id'] ?? null;
+ $direction = $matches['direction'] ?? null;
+ $count = $matches['count'] ?? null;
+
+ $storage = $clockwork->getStorage();
+
+ if ($direction === 'previous') {
+ $data = $storage->previous($id, $count);
+ } elseif ($direction === 'next') {
+ $data = $storage->next($id, $count);
+ } elseif ($id === 'latest') {
+ $data = $storage->latest();
+ } else {
+ $data = $storage->find($id);
+ }
+
+ if (preg_match('#(?[0-9-]+|latest)/extended#', $path)) {
+ $clockwork->extendRequest($data);
+ }
+
+ if (!$data) {
+ $response = ['message' => 'Not Found'];
+
+ return new Response(404, $headers, json_encode($response));
+ }
+
+ $data = is_array($data) ? array_map(static function ($item) {
+ return $item->toArray();
+ }, $data) : $data->toArray();
+
+ return new Response(200, $headers, json_encode($data));
+ }
+
+ /**
+ * @return void
+ */
+ protected function addMeasures(): void
+ {
+ if (!$this->enabled) {
+ return;
+ }
+
+ $nowTime = microtime(true);
+ $clkTimeLine = $this->clockwork ? $this->clockwork->timeline() : null;
+ $debTimeLine = $this->debugbar ? $this->debugbar['time'] : null;
+ foreach ($this->timers as $name => $data) {
+ $description = $data[0];
+ $startTime = $data[1] ?? null;
+ $endTime = $data[2] ?? $nowTime;
+ if ($clkTimeLine) {
+ $event = $clkTimeLine->event($description);
+ $event->finalize($startTime, $endTime);
+ } elseif ($debTimeLine) {
+ if ($endTime - $startTime < 0.001) {
+ continue;
+ }
+
+ $debTimeLine->addMeasure($description ?? $name, $startTime, $endTime);
+ }
+ }
+ $this->timers = [];
+ }
+
/**
* Set/get the enabled state of the debugger
*
- * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state
- *
+ * @param bool|null $state If null, the method returns the enabled value. If set, the method sets the enabled state
* @return bool
*/
public function enabled($state = null)
@@ -142,8 +391,7 @@ class Debugger
*/
public function addAssets()
{
- if ($this->enabled()) {
-
+ if ($this->enabled) {
// Only add assets if Page is HTML
$page = $this->grav['page'];
if ($page->templateFormat() !== 'html') {
@@ -153,28 +401,42 @@ class Debugger
/** @var Assets $assets */
$assets = $this->grav['assets'];
- // Add jquery library
- $assets->add('jquery', 101);
+ // Clockwork specific assets
+ if ($this->clockwork) {
+ $assets->addCss('/system/assets/debugger/clockwork.css', ['loading' => 'inline']);
+ $assets->addJs('/system/assets/debugger/clockwork.js', ['loading' => 'inline']);
+ }
- $this->renderer = $this->debugbar->getJavascriptRenderer();
- $this->renderer->setIncludeVendors(false);
- // Get the required CSS files
- list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
- foreach ((array)$css_files as $css) {
- $assets->addCss($css);
- }
+ // Debugbar specific assets
+ if ($this->debugbar) {
+ // Add jquery library
+ $assets->add('jquery', 101);
+
+ $this->renderer = $this->debugbar->getJavascriptRenderer();
+ $this->renderer->setIncludeVendors(false);
+
+ [$css_files, $js_files] = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
- $assets->addCss('/system/assets/debugger.css');
+ foreach ((array)$css_files as $css) {
+ $assets->addCss($css);
+ }
+
+ $assets->addCss('/system/assets/debugger/phpdebugbar.css', ['loading' => 'inline']);
- foreach ((array)$js_files as $js) {
- $assets->addJs($js);
+ foreach ((array)$js_files as $js) {
+ $assets->addJs($js);
+ }
}
}
return $this;
}
+ /**
+ * @param int $limit
+ * @return array
+ */
public function getCaller($limit = 2)
{
$trace = debug_backtrace(false, $limit);
@@ -186,13 +448,14 @@ class Debugger
* Adds a data collector
*
* @param DataCollectorInterface $collector
- *
* @return $this
- * @throws \DebugBar\DebugBarException
+ * @throws DebugBarException
*/
public function addCollector($collector)
{
- $this->debugbar->addCollector($collector);
+ if ($this->debugbar && !$this->debugbar->hasCollector($collector->getName())) {
+ $this->debugbar->addCollector($collector);
+ }
return $this;
}
@@ -200,14 +463,17 @@ class Debugger
/**
* Returns a data collector
*
- * @param DataCollectorInterface $collector
- *
- * @return DataCollectorInterface
- * @throws \DebugBar\DebugBarException
+ * @param string $name
+ * @return DataCollectorInterface|null
+ * @throws DebugBarException
*/
- public function getCollector($collector)
+ public function getCollector($name)
{
- return $this->debugbar->getCollector($collector);
+ if ($this->debugbar && $this->debugbar->hasCollector($name)) {
+ return $this->debugbar->getCollector($name);
+ }
+
+ return null;
}
/**
@@ -217,13 +483,14 @@ class Debugger
*/
public function render()
{
- if ($this->enabled()) {
+ if ($this->enabled && $this->debugbar) {
// Only add assets if Page is HTML
$page = $this->grav['page'];
if (!$this->renderer || $page->templateFormat() !== 'html') {
return $this;
}
+ $this->addMeasures();
$this->addDeprecations();
echo $this->renderer->render();
@@ -239,7 +506,8 @@ class Debugger
*/
public function sendDataInHeaders()
{
- if ($this->enabled()) {
+ if ($this->enabled && $this->debugbar) {
+ $this->addMeasures();
$this->addDeprecations();
$this->debugbar->sendDataInHeaders();
}
@@ -250,34 +518,182 @@ class Debugger
/**
* Returns collected debugger data.
*
- * @return array
+ * @return array|null
*/
public function getData()
{
- if (!$this->enabled()) {
+ if (!$this->enabled || !$this->debugbar) {
return null;
}
+ $this->addMeasures();
$this->addDeprecations();
$this->timers = [];
return $this->debugbar->getData();
}
+ /**
+ * Hierarchical Profiler support.
+ *
+ * @param callable $callable
+ * @param string|null $message
+ * @return mixed
+ */
+ public function profile(callable $callable, string $message = null)
+ {
+ $this->startProfiling();
+ $response = $callable();
+ $this->stopProfiling($message);
+
+ return $response;
+ }
+
+ public function addTwigProfiler(Environment $twig): void
+ {
+ $clockwork = $this->getClockwork();
+ if ($clockwork) {
+ $source = new TwigClockworkDataSource($twig);
+ $source->listenToEvents();
+ $clockwork->addDataSource($source);
+ }
+ }
+
+ /**
+ * Start profiling code.
+ *
+ * @return void
+ */
+ public function startProfiling(): void
+ {
+ if ($this->enabled && extension_loaded('tideways_xhprof')) {
+ $this->profiling++;
+ if ($this->profiling === 1) {
+ // @phpstan-ignore-next-line
+ \tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_NO_BUILTINS);
+ }
+ }
+ }
+
+ /**
+ * Stop profiling code. Returns profiling array or null if profiling couldn't be done.
+ *
+ * @param string|null $message
+ * @return array|null
+ */
+ public function stopProfiling(string $message = null): ?array
+ {
+ $timings = null;
+ if ($this->enabled && extension_loaded('tideways_xhprof')) {
+ $profiling = $this->profiling - 1;
+ if ($profiling === 0) {
+ // @phpstan-ignore-next-line
+ $timings = \tideways_xhprof_disable();
+ $timings = $this->buildProfilerTimings($timings);
+
+ if ($this->clockwork) {
+ /** @var UserData $userData */
+ $userData = $this->clockwork->userData('Profiler');
+ $userData->counters([
+ 'Calls' => count($timings)
+ ]);
+ $userData->table('Profiler', $timings);
+ } else {
+ $this->addMessage($message ?? 'Profiler Analysis', 'debug', $timings);
+ }
+ }
+ $this->profiling = max(0, $profiling);
+ }
+
+ return $timings;
+ }
+
+ /**
+ * @param array $timings
+ * @return array
+ */
+ protected function buildProfilerTimings(array $timings): array
+ {
+ // Filter method calls which take almost no time.
+ $timings = array_filter($timings, function ($value) {
+ return $value['wt'] > 50;
+ });
+
+ uasort($timings, function (array $a, array $b) {
+ return $b['wt'] <=> $a['wt'];
+ });
+
+ $table = [];
+ foreach ($timings as $key => $timing) {
+ $parts = explode('==>', $key);
+ $method = $this->parseProfilerCall(array_pop($parts));
+ $context = $this->parseProfilerCall(array_pop($parts));
+
+ // Skip redundant method calls.
+ if ($context === 'Grav\Framework\RequestHandler\RequestHandler::handle()') {
+ continue;
+ }
+
+ // Do not profile library calls.
+ if (strpos($context, 'Grav\\') !== 0) {
+ continue;
+ }
+
+ $table[] = [
+ 'Context' => $context,
+ 'Method' => $method,
+ 'Calls' => $timing['ct'],
+ 'Time (ms)' => $timing['wt'] / 1000,
+ ];
+ }
+
+ return $table;
+ }
+
+ /**
+ * @param string|null $call
+ * @return mixed|string|null
+ */
+ protected function parseProfilerCall(?string $call)
+ {
+ if (null === $call) {
+ return '';
+ }
+ if (strpos($call, '@')) {
+ [$call,] = explode('@', $call);
+ }
+ if (strpos($call, '::')) {
+ [$class, $call] = explode('::', $call);
+ }
+
+ if (!isset($class)) {
+ return $call;
+ }
+
+ // It is also possible to display twig files, but they are being logged in views.
+ /*
+ if (strpos($class, '__TwigTemplate_') === 0 && class_exists($class)) {
+ $env = new Environment();
+ / ** @var Template $template * /
+ $template = new $class($env);
+
+ return $template->getTemplateName();
+ }
+ */
+
+ return "{$class}::{$call}()";
+ }
+
/**
* Start a timer with an associated name and description
*
* @param string $name
* @param string|null $description
- *
* @return $this
*/
public function startTimer($name, $description = null)
{
- if (strpos($name, '_') === 0 || $this->enabled()) {
- $this->debugbar['time']->startMeasure($name, $description);
- $this->timers[] = $name;
- }
+ $this->timers[$name] = [$description, microtime(true)];
return $this;
}
@@ -286,13 +702,13 @@ class Debugger
* Stop the named timer
*
* @param string $name
- *
* @return $this
*/
public function stopTimer($name)
{
- if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) {
- $this->debugbar['time']->stopMeasure($name);
+ if (isset($this->timers[$name])) {
+ $endTime = microtime(true);
+ $this->timers[$name][] = $endTime;
}
return $this;
@@ -303,14 +719,76 @@ class Debugger
*
* @param mixed $message
* @param string $label
- * @param bool $isString
- *
+ * @param mixed|bool $isString
* @return $this
*/
public function addMessage($message, $label = 'info', $isString = true)
{
- if ($this->enabled()) {
- $this->debugbar['messages']->addMessage($message, $label, $isString);
+ if ($this->enabled) {
+ if ($this->censored) {
+ if (!is_scalar($message)) {
+ $message = 'CENSORED';
+ }
+ if (!is_scalar($isString)) {
+ $isString = ['CENSORED'];
+ }
+ }
+
+ if ($this->debugbar) {
+ if (is_array($isString)) {
+ $message = $isString;
+ $isString = false;
+ } elseif (is_string($isString)) {
+ $message = $isString;
+ $isString = true;
+ }
+ $this->debugbar['messages']->addMessage($message, $label, $isString);
+ }
+
+ if ($this->clockwork) {
+ $context = $isString;
+ if (!is_scalar($message)) {
+ $context = $message;
+ $message = gettype($context);
+ }
+ if (is_bool($context)) {
+ $context = [];
+ } elseif (!is_array($context)) {
+ $type = gettype($context);
+ $context = [$type => $context];
+ }
+
+ $this->clockwork->log($label, $message, $context);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ * @param object $event
+ * @param EventDispatcherInterface $dispatcher
+ * @param float|null $time
+ * @return $this
+ */
+ public function addEvent(string $name, $event, EventDispatcherInterface $dispatcher, float $time = null)
+ {
+ if ($this->enabled && $this->clockwork) {
+ $time = $time ?? microtime(true);
+ $duration = (microtime(true) - $time) * 1000;
+
+ $data = null;
+ if ($event && method_exists($event, '__debugInfo')) {
+ $data = $event;
+ }
+
+ $listeners = [];
+ foreach ($dispatcher->getListeners($name) as $listener) {
+ $listeners[] = $this->resolveCallable($listener);
+ }
+
+ $this->clockwork->addEvent($name, $data, $time, ['listeners' => $listeners, 'duration' => $duration]);
}
return $this;
@@ -319,18 +797,31 @@ class Debugger
/**
* Dump exception into the Messages tab of the Debug Bar
*
- * @param \Exception $e
+ * @param Throwable $e
* @return Debugger
*/
- public function addException(\Exception $e)
+ public function addException(Throwable $e)
{
- if ($this->initialized && $this->enabled()) {
- $this->debugbar['exceptions']->addException($e);
+ if ($this->initialized && $this->enabled) {
+ if ($this->debugbar) {
+ $this->debugbar['exceptions']->addThrowable($e);
+ }
+
+ if ($this->clockwork) {
+ /** @var UserData $exceptions */
+ $exceptions = $this->clockwork->userData('Exceptions');
+ $exceptions->data(['message' => $e->getMessage()]);
+
+ $this->clockwork->alert($e->getMessage(), ['exception' => $e]);
+ }
}
return $this;
}
+ /**
+ * @return void
+ */
public function setErrorHandler()
{
$this->errorHandler = set_error_handler(
@@ -349,13 +840,13 @@ class Debugger
{
if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) {
if ($this->errorHandler) {
- return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
+ return call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
}
return true;
}
- if (!$this->enabled()) {
+ if (!$this->enabled) {
return true;
}
@@ -382,10 +873,10 @@ class Debugger
foreach ($backtrace as $current) {
if (isset($current['args'])) {
foreach ($current['args'] as $arg) {
- if ($arg instanceof \SplFileInfo) {
+ if ($arg instanceof SplFileInfo) {
$arg = $arg->getPathname();
}
- if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
+ if (is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
$errfile = $arg;
$errline = 0;
@@ -403,18 +894,18 @@ class Debugger
if (isset($current['args'])) {
$args = [];
foreach ($current['args'] as $arg) {
- if (\is_string($arg)) {
+ if (is_string($arg)) {
$arg = "'" . $arg . "'";
if (mb_strlen($arg) > 100) {
$arg = 'string';
}
- } elseif (\is_bool($arg)) {
+ } elseif (is_bool($arg)) {
$arg = $arg ? 'true' : 'false';
- } elseif (\is_scalar($arg)) {
+ } elseif (is_scalar($arg)) {
$arg = $arg;
- } elseif (\is_object($arg)) {
+ } elseif (is_object($arg)) {
$arg = get_class($arg) . ' $object';
- } elseif (\is_array($arg)) {
+ } elseif (is_array($arg)) {
$arg = '$array';
} else {
$arg = '$object';
@@ -430,7 +921,7 @@ class Debugger
$reflection = null;
if ($object instanceof TemplateWrapper) {
- $reflection = new \ReflectionObject($object);
+ $reflection = new ReflectionObject($object);
$property = $reflection->getProperty('template');
$property->setAccessible(true);
$object = $property->getValue($object);
@@ -540,6 +1031,28 @@ class Debugger
return true;
}
+ /**
+ * @return array
+ */
+ protected function getDeprecations(): array
+ {
+ if (!$this->deprecations) {
+ return [];
+ }
+
+ $list = [];
+ /** @var array $deprecated */
+ foreach ($this->deprecations as $deprecated) {
+ $list[] = $this->getDepracatedMessage($deprecated)[0];
+ }
+
+ return $list;
+ }
+
+ /**
+ * @return void
+ * @throws DebugBarException
+ */
protected function addDeprecations()
{
if (!$this->deprecations) {
@@ -558,6 +1071,10 @@ class Debugger
}
}
+ /**
+ * @param array $deprecated
+ * @return array
+ */
protected function getDepracatedMessage($deprecated)
{
$scope = $deprecated['scope'];
@@ -595,6 +1112,10 @@ class Debugger
];
}
+ /**
+ * @param array $trace
+ * @return string
+ */
protected function getFunction($trace)
{
if (!isset($trace['function'])) {
@@ -603,4 +1124,17 @@ class Debugger
return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
}
+
+ /**
+ * @param callable $callable
+ * @return string
+ */
+ protected function resolveCallable(callable $callable)
+ {
+ if (is_array($callable)) {
+ return get_class($callable[0]) . '->' . $callable[1] . '()';
+ }
+
+ return 'unknown';
+ }
}
diff --git a/system/src/Grav/Common/Errors/BareHandler.php b/system/src/Grav/Common/Errors/BareHandler.php
index 52effc6d..206b57e8 100644
--- a/system/src/Grav/Common/Errors/BareHandler.php
+++ b/system/src/Grav/Common/Errors/BareHandler.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Errors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,22 +11,23 @@ namespace Grav\Common\Errors;
use Whoops\Handler\Handler;
+/**
+ * Class BareHandler
+ * @package Grav\Common\Errors
+ */
class BareHandler extends Handler
{
-
/**
- * @return int|null
+ * @return int
*/
public function handle()
{
$inspector = $this->getInspector();
$code = $inspector->getException()->getCode();
- if ( ($code >= 400) && ($code < 600) )
- {
- $this->getRun()->sendHttpCode($code);
+ if (($code >= 400) && ($code < 600)) {
+ $this->getRun()->sendHttpCode($code);
}
return Handler::QUIT;
}
-
}
diff --git a/system/src/Grav/Common/Errors/Errors.php b/system/src/Grav/Common/Errors/Errors.php
index dbc955ed..3dc99c66 100644
--- a/system/src/Grav/Common/Errors/Errors.php
+++ b/system/src/Grav/Common/Errors/Errors.php
@@ -3,17 +3,29 @@
/**
* @package Grav\Common\Errors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Errors;
+use Exception;
use Grav\Common\Grav;
-use Whoops;
+use Whoops\Handler\JsonResponseHandler;
+use Whoops\Handler\PrettyPageHandler;
+use Whoops\Run;
+use Whoops\Util\Misc;
+use function is_int;
+/**
+ * Class Errors
+ * @package Grav\Common\Errors
+ */
class Errors
{
+ /**
+ * @return void
+ */
public function resetHandlers()
{
$grav = Grav::instance();
@@ -22,7 +34,7 @@ class Errors
// Setup Whoops-based error handler
$system = new SystemFacade;
- $whoops = new Whoops\Run($system);
+ $whoops = new Run($system);
$verbosity = 1;
@@ -36,30 +48,30 @@ class Errors
switch ($verbosity) {
case 1:
- $error_page = new Whoops\Handler\PrettyPageHandler;
+ $error_page = new PrettyPageHandler();
$error_page->setPageTitle('Crikey! There was an error...');
$error_page->addResourcePath(GRAV_ROOT . '/system/assets');
$error_page->addCustomCss('whoops.css');
- $whoops->pushHandler($error_page);
+ $whoops->prependHandler($error_page);
break;
case -1:
- $whoops->pushHandler(new BareHandler);
+ $whoops->prependHandler(new BareHandler);
break;
default:
- $whoops->pushHandler(new SimplePageHandler);
+ $whoops->prependHandler(new SimplePageHandler);
break;
}
- if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
- $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
+ if ($jsonRequest || Misc::isAjaxRequest()) {
+ $whoops->prependHandler(new JsonResponseHandler());
}
if (isset($config['log']) && $config['log']) {
$logger = $grav['log'];
- $whoops->pushHandler(function($exception, $inspector, $run) use ($logger) {
+ $whoops->pushHandler(function ($exception, $inspector, $run) use ($logger) {
try {
$logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
- } catch (\Exception $e) {
+ } catch (Exception $e) {
echo $e;
}
});
diff --git a/system/src/Grav/Common/Errors/SimplePageHandler.php b/system/src/Grav/Common/Errors/SimplePageHandler.php
index 311d7224..0118929a 100644
--- a/system/src/Grav/Common/Errors/SimplePageHandler.php
+++ b/system/src/Grav/Common/Errors/SimplePageHandler.php
@@ -3,20 +3,29 @@
/**
* @package Grav\Common\Errors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Errors;
+use ErrorException;
+use InvalidArgumentException;
+use RuntimeException;
use Whoops\Handler\Handler;
use Whoops\Util\Misc;
use Whoops\Util\TemplateHelper;
+/**
+ * Class SimplePageHandler
+ * @package Grav\Common\Errors
+ */
class SimplePageHandler extends Handler
{
- private $searchPaths = array();
- private $resourceCache = array();
+ /** @var array */
+ private $searchPaths = [];
+ /** @var array */
+ private $resourceCache = [];
public function __construct()
{
@@ -25,7 +34,7 @@ class SimplePageHandler extends Handler
}
/**
- * @return int|null
+ * @return int
*/
public function handle()
{
@@ -36,13 +45,12 @@ class SimplePageHandler extends Handler
$cssFile = $this->getResource('error.css');
$code = $inspector->getException()->getCode();
- if ( ($code >= 400) && ($code < 600) )
- {
- $this->getRun()->sendHttpCode($code);
+ if (($code >= 400) && ($code < 600)) {
+ $this->getRun()->sendHttpCode($code);
}
$message = $inspector->getException()->getMessage();
- if ($inspector->getException() instanceof \ErrorException) {
+ if ($inspector->getException() instanceof ErrorException) {
$code = Misc::translateErrorCode($code);
}
@@ -60,9 +68,8 @@ class SimplePageHandler extends Handler
/**
* @param string $resource
- *
* @return string
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
protected function getResource($resource)
{
@@ -85,15 +92,19 @@ class SimplePageHandler extends Handler
}
// If we got this far, nothing was found.
- throw new \RuntimeException(
+ throw new RuntimeException(
"Could not find resource '{$resource}' in any resource paths (searched: " . implode(', ', $this->searchPaths). ')'
);
}
+ /**
+ * @param string $path
+ * @return void
+ */
public function addResourcePath($path)
{
if (!is_dir($path)) {
- throw new \InvalidArgumentException(
+ throw new InvalidArgumentException(
"'{$path}' is not a valid directory"
);
}
@@ -101,6 +112,9 @@ class SimplePageHandler extends Handler
array_unshift($this->searchPaths, $path);
}
+ /**
+ * @return array
+ */
public function getResourcePaths()
{
return $this->searchPaths;
diff --git a/system/src/Grav/Common/Errors/SystemFacade.php b/system/src/Grav/Common/Errors/SystemFacade.php
index 02ef0cf8..8a7f1ceb 100644
--- a/system/src/Grav/Common/Errors/SystemFacade.php
+++ b/system/src/Grav/Common/Errors/SystemFacade.php
@@ -3,19 +3,23 @@
/**
* @package Grav\Common\Errors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Errors;
+/**
+ * Class SystemFacade
+ * @package Grav\Common\Errors
+ */
class SystemFacade extends \Whoops\Util\SystemFacade
{
+ /** @var callable */
protected $whoopsShutdownHandler;
/**
* @param callable $function
- *
* @return void
*/
public function registerShutdownFunction(callable $function)
@@ -26,6 +30,8 @@ class SystemFacade extends \Whoops\Util\SystemFacade
/**
* Special case to deal with Fatal errors and the like.
+ *
+ * @return void
*/
public function handleShutdown()
{
diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php
index f4b1e8d4..66fa6201 100644
--- a/system/src/Grav/Common/File/CompiledFile.php
+++ b/system/src/Grav/Common/File/CompiledFile.php
@@ -3,21 +3,30 @@
/**
* @package Grav\Common\File
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\File;
+use Exception;
use RocketTheme\Toolbox\File\PhpFile;
+use RuntimeException;
+use Throwable;
+use function function_exists;
+use function get_class;
+/**
+ * Trait CompiledFile
+ * @package Grav\Common\File
+ */
trait CompiledFile
{
/**
* Get/set parsed file contents.
*
* @param mixed $var
- * @return string
+ * @return array
*/
public function content($var = null)
{
@@ -28,9 +37,12 @@ trait CompiledFile
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified();
-
if (!$modified) {
- return $this->decode($this->raw());
+ try {
+ return $this->decode($this->raw());
+ } catch (Throwable $e) {
+ // If the compiled file is broken, we can safely ignore the error and continue.
+ }
}
$class = get_class($this);
@@ -38,8 +50,7 @@ trait CompiledFile
$cache = $file->exists() ? $file->content() : null;
// Load real file if cache isn't up to date (or is invalid).
- if (
- !isset($cache['@class'])
+ if (!isset($cache['@class'])
|| $cache['@class'] !== $class
|| $cache['modified'] !== $modified
|| $cache['filename'] !== $this->filename
@@ -47,7 +58,7 @@ trait CompiledFile
// Attempt to lock the file for writing.
try {
$file->lock(false);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// Another process has locked the file; we will check this in a bit.
}
@@ -76,9 +87,8 @@ trait CompiledFile
$this->content = $cache['data'];
}
-
- } catch (\Exception $e) {
- throw new \RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
+ } catch (Exception $e) {
+ throw new RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
}
return parent::content($var);
@@ -86,6 +96,8 @@ trait CompiledFile
/**
* Serialize file.
+ *
+ * @return array
*/
public function __sleep()
{
diff --git a/system/src/Grav/Common/File/CompiledJsonFile.php b/system/src/Grav/Common/File/CompiledJsonFile.php
index d73e817b..dae95baa 100644
--- a/system/src/Grav/Common/File/CompiledJsonFile.php
+++ b/system/src/Grav/Common/File/CompiledJsonFile.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\File
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,10 @@ namespace Grav\Common\File;
use RocketTheme\Toolbox\File\JsonFile;
+/**
+ * Class CompiledJsonFile
+ * @package Grav\Common\File
+ */
class CompiledJsonFile extends JsonFile
{
use CompiledFile;
@@ -20,7 +24,7 @@ class CompiledJsonFile extends JsonFile
*
* @param string $var
* @param bool $assoc
- * @return array mixed
+ * @return array
*/
protected function decode($var, $assoc = true)
{
diff --git a/system/src/Grav/Common/File/CompiledMarkdownFile.php b/system/src/Grav/Common/File/CompiledMarkdownFile.php
index 74b28201..1107417d 100644
--- a/system/src/Grav/Common/File/CompiledMarkdownFile.php
+++ b/system/src/Grav/Common/File/CompiledMarkdownFile.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\File
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,10 @@ namespace Grav\Common\File;
use RocketTheme\Toolbox\File\MarkdownFile;
+/**
+ * Class CompiledMarkdownFile
+ * @package Grav\Common\File
+ */
class CompiledMarkdownFile extends MarkdownFile
{
use CompiledFile;
diff --git a/system/src/Grav/Common/File/CompiledYamlFile.php b/system/src/Grav/Common/File/CompiledYamlFile.php
index 2eb1e983..0fedeb71 100644
--- a/system/src/Grav/Common/File/CompiledYamlFile.php
+++ b/system/src/Grav/Common/File/CompiledYamlFile.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\File
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,10 @@ namespace Grav\Common\File;
use RocketTheme\Toolbox\File\YamlFile;
+/**
+ * Class CompiledYamlFile
+ * @package Grav\Common\File
+ */
class CompiledYamlFile extends YamlFile
{
use CompiledFile;
diff --git a/system/src/Grav/Common/Filesystem/Archiver.php b/system/src/Grav/Common/Filesystem/Archiver.php
index 50e40fbe..090bc13f 100644
--- a/system/src/Grav/Common/Filesystem/Archiver.php
+++ b/system/src/Grav/Common/Filesystem/Archiver.php
@@ -3,23 +3,37 @@
/**
* @package Grav\Common\Filesystem
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Filesystem;
+use FilesystemIterator;
use Grav\Common\Utils;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use function function_exists;
+/**
+ * Class Archiver
+ * @package Grav\Common\Filesystem
+ */
abstract class Archiver
{
+ /** @var array */
protected $options = [
'exclude_files' => ['.DS_Store'],
'exclude_paths' => []
];
+ /** @var string */
protected $archive_file;
+ /**
+ * @param string $compression
+ * @return ZipArchiver
+ */
public static function create($compression)
{
if ($compression === 'zip') {
@@ -29,38 +43,66 @@ abstract class Archiver
return new ZipArchiver();
}
+ /**
+ * @param string $archive_file
+ * @return $this
+ */
public function setArchive($archive_file)
{
$this->archive_file = $archive_file;
+
return $this;
}
+ /**
+ * @param array $options
+ * @return $this
+ */
public function setOptions($options)
{
// Set infinite PHP execution time if possible.
- if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) {
- set_time_limit(0);
+ if (Utils::functionExists('set_time_limit')) {
+ @set_time_limit(0);
}
$this->options = $options + $this->options;
+
return $this;
}
- public abstract function compress($folder, callable $status = null);
-
- public abstract function extract($destination, callable $status = null);
-
- public abstract function addEmptyFolders($folders, callable $status = null);
-
+ /**
+ * @param string $folder
+ * @param callable|null $status
+ * @return $this
+ */
+ abstract public function compress($folder, callable $status = null);
+
+ /**
+ * @param string $destination
+ * @param callable|null $status
+ * @return $this
+ */
+ abstract public function extract($destination, callable $status = null);
+
+ /**
+ * @param array $folders
+ * @param callable|null $status
+ * @return $this
+ */
+ abstract public function addEmptyFolders($folders, callable $status = null);
+
+ /**
+ * @param string $rootPath
+ * @return RecursiveIteratorIterator
+ */
protected function getArchiveFiles($rootPath)
{
$exclude_paths = $this->options['exclude_paths'];
$exclude_files = $this->options['exclude_files'];
- $dirItr = new \RecursiveDirectoryIterator($rootPath, \RecursiveDirectoryIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::UNIX_PATHS);
+ $dirItr = new RecursiveDirectoryIterator($rootPath, RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS);
$filterItr = new RecursiveDirectoryFilterIterator($dirItr, $rootPath, $exclude_paths, $exclude_files);
- $files = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
+ $files = new RecursiveIteratorIterator($filterItr, RecursiveIteratorIterator::SELF_FIRST);
return $files;
}
-
}
diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php
index 3196b65b..ecbc6cea 100644
--- a/system/src/Grav/Common/Filesystem/Folder.php
+++ b/system/src/Grav/Common/Filesystem/Folder.php
@@ -3,15 +3,29 @@
/**
* @package Grav\Common\Filesystem
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Filesystem;
+use DirectoryIterator;
+use Exception;
+use FilesystemIterator;
use Grav\Common\Grav;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use function count;
+use function dirname;
+use function is_callable;
+/**
+ * Class Folder
+ * @package Grav\Common\Filesystem
+ */
abstract class Folder
{
/**
@@ -30,16 +44,15 @@ abstract class Folder
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
+ $flags = RecursiveDirectoryIterator::SKIP_DOTS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
- $directory = new \RecursiveDirectoryIterator($path, $flags);
+ $directory = new RecursiveDirectoryIterator($path, $flags);
}
$filter = new RecursiveFolderFilterIterator($directory);
- $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST);
+ $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
- /** @var \RecursiveDirectoryIterator $file */
foreach ($iterator as $dir) {
$dir_modified = $dir->getMTime();
if ($dir_modified > $last_modified) {
@@ -55,7 +68,6 @@ abstract class Folder
*
* @param string $path
* @param string $extensions which files to search for specifically
- *
* @return int
*/
public static function lastModifiedFile($path, $extensions = 'md|yaml')
@@ -68,23 +80,23 @@ abstract class Folder
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
+ $flags = RecursiveDirectoryIterator::SKIP_DOTS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
- $directory = new \RecursiveDirectoryIterator($path, $flags);
+ $directory = new RecursiveDirectoryIterator($path, $flags);
}
- $recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
- $iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
+ $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
+ $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
- /** @var \RecursiveDirectoryIterator $file */
+ /** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $filepath => $file) {
try {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
}
}
@@ -103,17 +115,17 @@ abstract class Folder
$files = [];
if (file_exists($path)) {
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
+ $flags = RecursiveDirectoryIterator::SKIP_DOTS;
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
- $directory = new \RecursiveDirectoryIterator($path, $flags);
+ $directory = new RecursiveDirectoryIterator($path, $flags);
}
- $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
+ $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
$files[] = $file->getPathname() . '?'. $file->getMTime();
@@ -126,9 +138,8 @@ abstract class Folder
/**
* Get relative path between target and base path. If path isn't relative, return full path.
*
- * @param string $path
- * @param mixed|string $base
- *
+ * @param string $path
+ * @param string $base
* @return string
*/
public static function getRelativePath($path, $base = GRAV_ROOT)
@@ -186,7 +197,7 @@ abstract class Folder
* Shift first directory out of the path.
*
* @param string $path
- * @return string
+ * @return string|null
*/
public static function shift(&$path)
{
@@ -203,12 +214,12 @@ abstract class Folder
* @param string $path
* @param array $params
* @return array
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public static function all($path, array $params = [])
{
- if ($path === false) {
- throw new \RuntimeException("Path doesn't exist.");
+ if (!$path) {
+ throw new RuntimeException("Path doesn't exist.");
}
if (!file_exists($path)) {
return [];
@@ -227,26 +238,26 @@ abstract class Folder
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($recursive) {
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
- + \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
+ $flags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
+ + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
- $directory = new \RecursiveDirectoryIterator($path, $flags);
+ $directory = new RecursiveDirectoryIterator($path, $flags);
}
- $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
+ $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
$iterator->setMaxDepth(max($levels, -1));
} else {
if ($locator->isStream($path)) {
$iterator = $locator->getIterator($path);
} else {
- $iterator = new \FilesystemIterator($path);
+ $iterator = new FilesystemIterator($path);
}
}
$results = [];
- /** @var \RecursiveDirectoryIterator $file */
+ /** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $file) {
// Ignore hidden files.
if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
@@ -293,8 +304,9 @@ abstract class Folder
*
* @param string $source
* @param string $target
- * @param string $ignore Ignore files matching pattern (regular expression).
- * @throws \RuntimeException
+ * @param string|null $ignore Ignore files matching pattern (regular expression).
+ * @return void
+ * @throws RuntimeException
*/
public static function copy($source, $target, $ignore = null)
{
@@ -302,7 +314,7 @@ abstract class Folder
$target = rtrim($target, '\\/');
if (!is_dir($source)) {
- throw new \RuntimeException('Cannot copy non-existing folder.');
+ throw new RuntimeException('Cannot copy non-existing folder.');
}
// Make sure that path to the target exists before copying.
@@ -332,7 +344,7 @@ abstract class Folder
if (!$success) {
$error = error_get_last();
- throw new \RuntimeException($error['message'] ?? 'Unknown error');
+ throw new RuntimeException($error['message'] ?? 'Unknown error');
}
// Make sure that the change will be detected when caching.
@@ -344,13 +356,14 @@ abstract class Folder
*
* @param string $source
* @param string $target
- * @throws \RuntimeException
+ * @return void
+ * @throws RuntimeException
*/
public static function move($source, $target)
{
if (!file_exists($source) || !is_dir($source)) {
// Rename fails if source folder does not exist.
- throw new \RuntimeException('Cannot move non-existing folder.');
+ throw new RuntimeException('Cannot move non-existing folder.');
}
// Don't do anything if the source is the same as the new target
@@ -358,9 +371,13 @@ abstract class Folder
return;
}
+ if (strpos($target, $source . '/') === 0) {
+ throw new RuntimeException('Cannot move folder to itself');
+ }
+
if (file_exists($target)) {
// Rename fails if target folder exists.
- throw new \RuntimeException('Cannot move files to existing folder/file.');
+ throw new RuntimeException('Cannot move files to existing folder/file.');
}
// Make sure that path to the target exists before moving.
@@ -370,11 +387,7 @@ abstract class Folder
@rename($source, $target);
// Rename function can fail while still succeeding, so let's check if the folder exists.
- if (!file_exists($target) || !is_dir($target)) {
- // In some rare cases rename() creates file, not a folder. Get rid of it.
- if (file_exists($target)) {
- @unlink($target);
- }
+ if (is_dir($source)) {
// Rename doesn't support moving folders across filesystems. Use copy instead.
self::copy($source, $target);
self::delete($source);
@@ -392,7 +405,7 @@ abstract class Folder
* @param string $target
* @param bool $include_target
* @return bool
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public static function delete($target, $include_target = true)
{
@@ -404,7 +417,8 @@ abstract class Folder
if (!$success) {
$error = error_get_last();
- throw new \RuntimeException($error['message']);
+
+ throw new RuntimeException($error['message'] ?? 'Unknown error');
}
// Make sure that the change will be detected when caching.
@@ -419,7 +433,8 @@ abstract class Folder
/**
* @param string $folder
- * @throws \RuntimeException
+ * @return void
+ * @throws RuntimeException
*/
public static function mkdir($folder)
{
@@ -428,7 +443,8 @@ abstract class Folder
/**
* @param string $folder
- * @throws \RuntimeException
+ * @return void
+ * @throws RuntimeException
*/
public static function create($folder)
{
@@ -443,7 +459,7 @@ abstract class Folder
// Take yet another look, make sure that the folder doesn't exist.
clearstatcache(true, $folder);
if (!@is_dir($folder)) {
- throw new \RuntimeException(sprintf('Unable to create directory: %s', $folder));
+ throw new RuntimeException(sprintf('Unable to create directory: %s', $folder));
}
}
}
@@ -453,9 +469,8 @@ abstract class Folder
*
* @param string $src
* @param string $dest
- *
* @return bool
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public static function rcopy($src, $dest)
{
@@ -472,8 +487,7 @@ abstract class Folder
}
// Open the source directory to read in files
- $i = new \DirectoryIterator($src);
- /** @var \DirectoryIterator $f */
+ $i = new DirectoryIterator($src);
foreach ($i as $f) {
if ($f->isFile()) {
copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
@@ -486,6 +500,22 @@ abstract class Folder
return 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);
+ }
+
/**
* @param string $folder
* @param bool $include_target
diff --git a/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
index c662fc46..0dac81c1 100644
--- a/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
+++ b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php
@@ -3,27 +3,39 @@
/**
* @package Grav\Common\Filesystem
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Filesystem;
-class RecursiveDirectoryFilterIterator extends \RecursiveFilterIterator
+use RecursiveFilterIterator;
+use RecursiveIterator;
+use SplFileInfo;
+use function in_array;
+
+/**
+ * Class RecursiveDirectoryFilterIterator
+ * @package Grav\Common\Filesystem
+ */
+class RecursiveDirectoryFilterIterator extends RecursiveFilterIterator
{
+ /** @var string */
protected static $root;
+ /** @var array */
protected static $ignore_folders;
+ /** @var array */
protected static $ignore_files;
/**
* Create a RecursiveFilterIterator from a RecursiveIterator
*
- * @param \RecursiveIterator $iterator
+ * @param RecursiveIterator $iterator
* @param string $root
* @param array $ignore_folders
* @param array $ignore_files
*/
- public function __construct(\RecursiveIterator $iterator, $root, $ignore_folders, $ignore_files)
+ public function __construct(RecursiveIterator $iterator, $root, $ignore_folders, $ignore_files)
{
parent::__construct($iterator);
@@ -39,7 +51,7 @@ class RecursiveDirectoryFilterIterator extends \RecursiveFilterIterator
*/
public function accept()
{
- /** @var \SplFileInfo $file */
+ /** @var SplFileInfo $file */
$file = $this->current();
$filename = $file->getFilename();
$relative_filename = str_replace($this::$root . '/', '', $file->getPathname());
@@ -57,6 +69,9 @@ class RecursiveDirectoryFilterIterator extends \RecursiveFilterIterator
return false;
}
+ /**
+ * @return RecursiveDirectoryFilterIterator|RecursiveFilterIterator
+ */
public function getChildren()
{
/** @var RecursiveDirectoryFilterIterator $iterator */
diff --git a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
index eb493db6..ded0259c 100644
--- a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
+++ b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
@@ -3,25 +3,33 @@
/**
* @package Grav\Common\Filesystem
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Filesystem;
use Grav\Common\Grav;
+use RecursiveIterator;
+use SplFileInfo;
+use function in_array;
+/**
+ * Class RecursiveFolderFilterIterator
+ * @package Grav\Common\Filesystem
+ */
class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
{
+ /** @var array */
protected static $ignore_folders;
/**
* Create a RecursiveFilterIterator from a RecursiveIterator
*
- * @param \RecursiveIterator $iterator
+ * @param RecursiveIterator $iterator
* @param array $ignore_folders
*/
- public function __construct(\RecursiveIterator $iterator, $ignore_folders = [])
+ public function __construct(RecursiveIterator $iterator, $ignore_folders = [])
{
parent::__construct($iterator);
@@ -39,7 +47,7 @@ class RecursiveFolderFilterIterator extends \RecursiveFilterIterator
*/
public function accept()
{
- /** @var \SplFileInfo $current */
+ /** @var SplFileInfo $current */
$current = $this->current();
return $current->isDir() && !in_array($current->getFilename(), $this::$ignore_folders, true);
diff --git a/system/src/Grav/Common/Filesystem/ZipArchiver.php b/system/src/Grav/Common/Filesystem/ZipArchiver.php
index 91212f77..6e53e70a 100644
--- a/system/src/Grav/Common/Filesystem/ZipArchiver.php
+++ b/system/src/Grav/Common/Filesystem/ZipArchiver.php
@@ -3,52 +3,71 @@
/**
* @package Grav\Common\Filesystem
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Filesystem;
+use InvalidArgumentException;
+use RuntimeException;
+use ZipArchive;
+use function extension_loaded;
+use function strlen;
+
+/**
+ * Class ZipArchiver
+ * @package Grav\Common\Filesystem
+ */
class ZipArchiver extends Archiver
{
-
+ /**
+ * @param string $destination
+ * @param callable|null $status
+ * @return $this
+ */
public function extract($destination, callable $status = null)
{
- $zip = new \ZipArchive();
+ $zip = new ZipArchive();
$archive = $zip->open($this->archive_file);
if ($archive === true) {
Folder::create($destination);
if (!$zip->extractTo($destination)) {
- throw new \RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);
+ throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);
}
$zip->close();
+
return $this;
}
- throw new \RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file);
+ throw new RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file);
}
+ /**
+ * @param string $source
+ * @param callable|null $status
+ * @return $this
+ */
public function compress($source, callable $status = null)
{
if (!extension_loaded('zip')) {
- throw new \InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
+ throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
}
- if (!file_exists($source)) {
- throw new \InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
+ // Get real path for our folder
+ $rootPath = realpath($source);
+ if (!$rootPath) {
+ throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
}
- $zip = new \ZipArchive();
- if (!$zip->open($this->archive_file, \ZipArchive::CREATE)) {
- throw new \InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
+ $zip = new ZipArchive();
+ if (!$zip->open($this->archive_file, ZipArchive::CREATE)) {
+ throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
}
- // Get real path for our folder
- $rootPath = realpath($source);
-
$files = $this->getArchiveFiles($rootPath);
$status && $status([
@@ -81,15 +100,20 @@ class ZipArchiver extends Archiver
return $this;
}
+ /**
+ * @param array $folders
+ * @param callable|null $status
+ * @return $this
+ */
public function addEmptyFolders($folders, callable $status = null)
{
if (!extension_loaded('zip')) {
- throw new \InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
+ throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
}
- $zip = new \ZipArchive();
+ $zip = new ZipArchive();
if (!$zip->open($this->archive_file)) {
- throw new \InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...');
+ throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...');
}
$status && $status([
@@ -97,7 +121,7 @@ class ZipArchiver extends Archiver
'message' => 'Adding empty folders...'
]);
- foreach($folders as $folder) {
+ foreach ($folders as $folder) {
$zip->addEmptyDir($folder);
$status && $status([
'type' => 'progress',
diff --git a/system/src/Grav/Common/Flex/FlexCollection.php b/system/src/Grav/Common/Flex/FlexCollection.php
new file mode 100644
index 00000000..6429c9e1
--- /dev/null
+++ b/system/src/Grav/Common/Flex/FlexCollection.php
@@ -0,0 +1,28 @@
+
+ */
+abstract class FlexCollection extends \Grav\Framework\Flex\FlexCollection
+{
+ use FlexGravTrait;
+ use FlexCollectionTrait;
+}
diff --git a/system/src/Grav/Common/Flex/FlexIndex.php b/system/src/Grav/Common/Flex/FlexIndex.php
new file mode 100644
index 00000000..44b6fdd6
--- /dev/null
+++ b/system/src/Grav/Common/Flex/FlexIndex.php
@@ -0,0 +1,29 @@
+
+ */
+abstract class FlexIndex extends \Grav\Framework\Flex\FlexIndex
+{
+ use FlexGravTrait;
+ use FlexIndexTrait;
+}
diff --git a/system/src/Grav/Common/Flex/FlexObject.php b/system/src/Grav/Common/Flex/FlexObject.php
new file mode 100644
index 00000000..b64aea1d
--- /dev/null
+++ b/system/src/Grav/Common/Flex/FlexObject.php
@@ -0,0 +1,74 @@
+getNestedProperty($name, null, $separator);
+
+ // Handle media order field.
+ if (null === $value && $name === 'media_order') {
+ return implode(',', $this->getMediaOrder());
+ }
+
+ // Handle media fields.
+ $settings = $this->getFieldSettings($name);
+ if (($settings['media_field'] ?? false) === true) {
+ return $this->parseFileProperty($value, $settings);
+ }
+
+ return $value ?? $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @see FlexObjectInterface::prepareStorage()
+ */
+ public function prepareStorage(): array
+ {
+ // Remove extra content from media fields.
+ $fields = $this->getMediaFields();
+ foreach ($fields as $field) {
+ $data = $this->getNestedProperty($field);
+ if (is_array($data)) {
+ foreach ($data as $name => &$image) {
+ unset($image['image_url'], $image['thumb_url']);
+ }
+ unset($image);
+ $this->setNestedProperty($field, $data);
+ }
+ }
+
+ return parent::prepareStorage();
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php b/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
new file mode 100644
index 00000000..29b640e7
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php
@@ -0,0 +1,51 @@
+ 'flex',
+ 'directory' => $this->getFlexDirectory(),
+ 'collection' => $this
+ ]);
+ }
+ if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
+ $name = 'onFlexCollection' . substr($name, 2);
+ }
+
+ $container = $this->getContainer();
+ if ($event instanceof Event) {
+ $container->fireEvent($name, $event);
+ } else {
+ $container->dispatchEvent($event);
+ }
+
+ return $this;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php b/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
new file mode 100644
index 00000000..1077e008
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php
@@ -0,0 +1,54 @@
+getContainer();
+
+ /** @var Twig $twig */
+ $twig = $container['twig'];
+
+ try {
+ return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
+ } catch (LoaderError $e) {
+ /** @var Debugger $debugger */
+ $debugger = Grav::instance()['debugger'];
+ $debugger->addException($e);
+
+ return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
+ }
+ }
+
+ abstract protected function getTemplatePaths(string $layout): array;
+ abstract protected function getContainer(): Grav;
+}
diff --git a/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php b/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
new file mode 100644
index 00000000..9d5c9e08
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php
@@ -0,0 +1,74 @@
+getContainer();
+
+ /** @var Flex $flex */
+ $flex = $container['flex'];
+
+ return $flex;
+ }
+
+ /**
+ * @return UserInterface|null
+ */
+ protected function getActiveUser(): ?UserInterface
+ {
+ $container = $this->getContainer();
+
+ /** @var UserInterface|null $user */
+ $user = $container['user'] ?? null;
+
+ return $user;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isAdminSite(): bool
+ {
+ $container = $this->getContainer();
+
+ return isset($container['admin']);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getAuthorizeScope(): string
+ {
+ return $this->isAdminSite() ? 'admin' : 'site';
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php b/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
new file mode 100644
index 00000000..1d0ee5cc
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php
@@ -0,0 +1,20 @@
+ 'onFlexObjectRender',
+ 'onBeforeSave' => 'onFlexObjectBeforeSave',
+ 'onAfterSave' => 'onFlexObjectAfterSave',
+ 'onBeforeDelete' => 'onFlexObjectBeforeDelete',
+ 'onAfterDelete' => 'onFlexObjectAfterDelete'
+ ];
+
+ if (null === $event) {
+ $event = new Event([
+ 'type' => 'flex',
+ 'directory' => $this->getFlexDirectory(),
+ 'object' => $this
+ ]);
+ }
+
+ if (isset($events['name'])) {
+ $name = $events['name'];
+ } elseif (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) {
+ $name = 'onFlexObject' . substr($name, 2);
+ }
+
+ $container = $this->getContainer();
+ if ($event instanceof Event) {
+ $container->fireEvent($name, $event);
+ } else {
+ $container->dispatchEvent($event);
+ }
+
+ return $this;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php b/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php
new file mode 100644
index 00000000..6b2fbc1e
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php
@@ -0,0 +1,24 @@
+
+ */
+class GenericCollection extends FlexCollection
+{
+}
diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php b/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
new file mode 100644
index 00000000..81dbf954
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php
@@ -0,0 +1,24 @@
+
+ */
+class GenericIndex extends FlexIndex
+{
+}
diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php b/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
new file mode 100644
index 00000000..4a567528
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php
@@ -0,0 +1,22 @@
+
+ *
+ * Incompatibilities with Grav\Common\Page\Collection:
+ * $page = $collection->key() will not work at all
+ * $clone = clone $collection does not clone objects inside the collection, does it matter?
+ * $string = (string)$collection returns collection id instead of comma separated list
+ * $collection->add() incompatible method signature
+ * $collection->remove() incompatible method signature
+ * $collection->filter() incompatible method signature (takes closure instead of callable)
+ * $collection->prev() does not rewind the internal pointer
+ * AND most methods are immutable; they do not update the current collection, but return updated one
+ *
+ * @method static shuffle()
+ * @method static select(array $keys)
+ * @method static unselect(array $keys)
+ * @method static createFrom(array $elements, string $keyField = null)
+ * @method PageIndex getIndex()
+ */
+class PageCollection extends FlexPageCollection implements PageCollectionInterface
+{
+ use FlexGravTrait;
+ use FlexCollectionTrait;
+
+ /** @var array|null */
+ protected $_params;
+
+ /**
+ * @return array
+ */
+ public static function getCachedMethods(): array
+ {
+ return [
+ // Collection specific methods
+ 'getRoot' => false,
+ 'getParams' => false,
+ 'setParams' => false,
+ 'params' => false,
+ 'addPage' => false,
+ 'merge' => false,
+ 'intersect' => false,
+ 'prev' => false,
+ 'nth' => false,
+ 'random' => false,
+ 'append' => false,
+ 'batch' => false,
+ 'order' => false,
+
+ // Collection filtering
+ 'dateRange' => true,
+ 'visible' => true,
+ 'nonVisible' => true,
+ 'pages' => true,
+ 'modules' => true,
+ 'modular' => true,
+ 'nonModular' => true,
+ 'published' => true,
+ 'nonPublished' => true,
+ 'routable' => true,
+ 'nonRoutable' => true,
+ 'ofType' => true,
+ 'ofOneOfTheseTypes' => true,
+ 'ofOneOfTheseAccessLevels' => true,
+ 'withOrdered' => true,
+ 'withModules' => true,
+ 'withPages' => true,
+ 'withTranslation' => true,
+ 'filterBy' => true,
+
+ 'toExtendedArray' => false,
+ 'getLevelListing' => false,
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * @return PageObject
+ */
+ public function getRoot()
+ {
+ $index = $this->getIndex();
+
+ return $index->getRoot();
+ }
+
+ /**
+ * Get the collection params
+ *
+ * @return array
+ */
+ public function getParams(): array
+ {
+ return $this->_params ?? [];
+ }
+
+ /**
+ * Set parameters to the Collection
+ *
+ * @param array $params
+ * @return $this
+ */
+ public function setParams(array $params)
+ {
+ $this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
+
+ return $this;
+ }
+
+ /**
+ * Get the collection params
+ *
+ * @return array
+ */
+ public function params(): array
+ {
+ return $this->getParams();
+ }
+
+ /**
+ * Add a single page to a collection
+ *
+ * @param PageInterface $page
+ * @return static
+ */
+ public function addPage(PageInterface $page)
+ {
+ if (!$page instanceof PageObject) {
+ throw new InvalidArgumentException('$page is not a flex page.');
+ }
+
+ // FIXME: support other keys.
+ $this->set($page->getKey(), $page);
+
+ return $this;
+ }
+
+ /**
+ *
+ * Merge another collection with the current collection
+ *
+ * @param PageCollectionInterface $collection
+ * @return static
+ */
+ public function merge(PageCollectionInterface $collection)
+ {
+ throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+ }
+
+ /**
+ * Intersect another collection with the current collection
+ *
+ * @param PageCollectionInterface $collection
+ * @return static
+ */
+ public function intersect(PageCollectionInterface $collection)
+ {
+ throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+ }
+
+ /**
+ * Set current page.
+ */
+ public function setCurrent(string $path): void
+ {
+ throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+ }
+
+ /**
+ * Return previous item.
+ *
+ * @return PageInterface|false
+ * @phpstan-return PageObject|false
+ */
+ public function prev()
+ {
+ // FIXME: this method does not rewind the internal pointer!
+ $key = (string)$this->key();
+ $prev = $this->prevSibling($key);
+
+ return $prev !== $this->current() ? $prev : false;
+ }
+
+ /**
+ * Return nth item.
+ * @param int $key
+ * @return PageInterface|bool
+ * @phpstan-return PageObject|false
+ */
+ public function nth($key)
+ {
+ return $this->slice($key, 1)[0] ?? false;
+ }
+
+ /**
+ * Pick one or more random entries.
+ *
+ * @param int $num Specifies how many entries should be picked.
+ * @return static
+ */
+ public function random($num = 1)
+ {
+ return $this->createFrom($this->shuffle()->slice(0, $num));
+ }
+
+ /**
+ * Append new elements to the list.
+ *
+ * @param array $items Items to be appended. Existing keys will be overridden with the new values.
+ * @return static
+ */
+ public function append($items)
+ {
+ throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+ }
+
+ /**
+ * Split collection into array of smaller collections.
+ *
+ * @param int $size
+ * @return static[]
+ */
+ public function batch($size): array
+ {
+ $chunks = $this->chunk($size);
+
+ $list = [];
+ foreach ($chunks as $chunk) {
+ $list[] = $this->createFrom($chunk);
+ }
+
+ return $list;
+ }
+
+ /**
+ * Reorder collection.
+ *
+ * @param string $by
+ * @param string $dir
+ * @param array|null $manual
+ * @param int|null $sort_flags
+ * @return static
+ */
+ public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
+ {
+ if (!$this->count()) {
+ return $this;
+ }
+
+ if ($by === 'random') {
+ return $this->shuffle();
+ }
+
+ $keys = $this->buildSort($by, $dir, $manual, $sort_flags);
+
+ return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []);
+ }
+
+ /**
+ * @param string $order_by
+ * @param string $order_dir
+ * @param array|null $manual
+ * @param int|null $sort_flags
+ * @return array
+ */
+ protected function buildSort($order_by = 'default', $order_dir = 'asc', $manual = null, $sort_flags = null): array
+ {
+ // do this header query work only once
+ $header_query = null;
+ $header_default = null;
+ if (strpos($order_by, 'header.') === 0) {
+ $query = explode('|', str_replace('header.', '', $order_by), 2);
+ $header_query = array_shift($query) ?? '';
+ $header_default = array_shift($query);
+ }
+
+ $list = [];
+ foreach ($this as $key => $child) {
+ switch ($order_by) {
+ case 'title':
+ $list[$key] = $child->title();
+ break;
+ case 'date':
+ $list[$key] = $child->date();
+ $sort_flags = SORT_REGULAR;
+ break;
+ case 'modified':
+ $list[$key] = $child->modified();
+ $sort_flags = SORT_REGULAR;
+ break;
+ case 'publish_date':
+ $list[$key] = $child->publishDate();
+ $sort_flags = SORT_REGULAR;
+ break;
+ case 'unpublish_date':
+ $list[$key] = $child->unpublishDate();
+ $sort_flags = SORT_REGULAR;
+ break;
+ case 'slug':
+ $list[$key] = $child->slug();
+ break;
+ case 'basename':
+ $list[$key] = basename($key);
+ break;
+ case 'folder':
+ $list[$key] = $child->folder();
+ break;
+ case 'manual':
+ case 'default':
+ default:
+ if (is_string($header_query)) {
+ /** @var Header $child_header */
+ $child_header = $child->header();
+ $header_value = $child_header->get($header_query);
+ if (is_array($header_value)) {
+ $list[$key] = implode(',', $header_value);
+ } elseif ($header_value) {
+ $list[$key] = $header_value;
+ } else {
+ $list[$key] = $header_default ?: $key;
+ }
+ $sort_flags = $sort_flags ?: SORT_REGULAR;
+ break;
+ }
+ $list[$key] = $key;
+ $sort_flags = $sort_flags ?: SORT_REGULAR;
+ }
+ }
+
+ if (null === $sort_flags) {
+ $sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
+ }
+
+ // else just sort the list according to specified key
+ if (extension_loaded('intl') && Grav::instance()['config']->get('system.intl_enabled')) {
+ $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set
+ $col = Collator::create($locale);
+ if ($col) {
+ $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
+ if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
+ $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
+ return sprintf('%032d.', $number[0]);
+ }, $list);
+ if (!is_array($list)) {
+ throw new RuntimeException('Internal Error');
+ }
+
+ $list_vals = array_values($list);
+ if (is_numeric(array_shift($list_vals))) {
+ $sort_flags = Collator::SORT_REGULAR;
+ } else {
+ $sort_flags = Collator::SORT_STRING;
+ }
+ }
+
+ $col->asort($list, $sort_flags);
+ } else {
+ asort($list, $sort_flags);
+ }
+ } else {
+ asort($list, $sort_flags);
+ }
+
+ // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
+ if (is_array($manual) && !empty($manual)) {
+ $i = count($manual);
+ $new_list = [];
+ foreach ($list as $key => $dummy) {
+ $child = $this[$key] ?? null;
+ $order = $child ? array_search($child->slug, $manual, true) : false;
+ if ($order === false) {
+ $order = $i++;
+ }
+ $new_list[$key] = (int)$order;
+ }
+
+ $list = $new_list;
+
+ // Apply manual ordering to the list.
+ asort($list, SORT_NUMERIC);
+ }
+
+ if ($order_dir !== 'asc') {
+ $list = array_reverse($list);
+ }
+
+ return array_keys($list);
+ }
+
+ /**
+ * Mimicks Pages class.
+ *
+ * @return $this
+ * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
+ */
+ public function all()
+ {
+ return $this;
+ }
+
+ /**
+ * Returns the items between a set of date ranges of either the page date field (default) or
+ * an arbitrary datetime page field where start date and end date are optional
+ * Dates must be passed in as text that strtotime() can process
+ * http://php.net/manual/en/function.strtotime.php
+ *
+ * @param string|null $startDate
+ * @param string|null $endDate
+ * @param string|null $field
+ * @return static
+ * @throws Exception
+ */
+ public function dateRange($startDate = null, $endDate = null, $field = null)
+ {
+ $start = $startDate ? Utils::date2timestamp($startDate) : null;
+ $end = $endDate ? Utils::date2timestamp($endDate) : null;
+
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if (!$object) {
+ continue;
+ }
+
+ $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
+
+ if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only visible pages
+ *
+ * @return static The collection with only visible pages
+ */
+ public function visible()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && $object->visible()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only non-visible pages
+ *
+ * @return static The collection with only non-visible pages
+ */
+ public function nonVisible()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && !$object->visible()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only pages
+ *
+ * @return static The collection with only pages
+ */
+ public function pages()
+ {
+ $entries = [];
+ /**
+ * @var int|string $key
+ * @var PageInterface|null $object
+ */
+ foreach ($this as $key => $object) {
+ if ($object && !$object->isModule()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only modules
+ *
+ * @return static The collection with only modules
+ */
+ public function modules()
+ {
+ $entries = [];
+ /**
+ * @var int|string $key
+ * @var PageInterface|null $object
+ */
+ foreach ($this as $key => $object) {
+ if ($object && $object->isModule()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Alias of modules()
+ *
+ * @return static
+ */
+ public function modular()
+ {
+ return $this->modules();
+ }
+
+ /**
+ * Alias of pages()
+ *
+ * @return static
+ */
+ public function nonModular()
+ {
+ return $this->pages();
+ }
+
+ /**
+ * Creates new collection with only published pages
+ *
+ * @return static The collection with only published pages
+ */
+ public function published()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && $object->published()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only non-published pages
+ *
+ * @return static The collection with only non-published pages
+ */
+ public function nonPublished()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && !$object->published()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only routable pages
+ *
+ * @return static The collection with only routable pages
+ */
+ public function routable()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && $object->routable()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only non-routable pages
+ *
+ * @return static The collection with only non-routable pages
+ */
+ public function nonRoutable()
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && !$object->routable()) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only pages of the specified type
+ *
+ * @param string $type
+ * @return static The collection
+ */
+ public function ofType($type)
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && $object->template() === $type) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only pages of one of the specified types
+ *
+ * @param string[] $types
+ * @return static The collection
+ */
+ public function ofOneOfTheseTypes($types)
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && in_array($object->template(), $types, true)) {
+ $entries[$key] = $object;
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * Creates new collection with only pages of one of the specified access levels
+ *
+ * @param array $accessLevels
+ * @return static The collection
+ */
+ public function ofOneOfTheseAccessLevels($accessLevels)
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object && isset($object->header()->access)) {
+ if (is_array($object->header()->access)) {
+ //Multiple values for access
+ $valid = false;
+
+ foreach ($object->header()->access as $index => $accessLevel) {
+ if (is_array($accessLevel)) {
+ foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
+ if (in_array($innerAccessLevel, $accessLevels)) {
+ $valid = true;
+ }
+ }
+ } else {
+ if (in_array($index, $accessLevels)) {
+ $valid = true;
+ }
+ }
+ }
+ if ($valid) {
+ $entries[$key] = $object;
+ }
+ } else {
+ //Single value for access
+ if (in_array($object->header()->access, $accessLevels)) {
+ $entries[$key] = $object;
+ }
+ }
+ }
+ }
+
+ return $this->createFrom($entries);
+ }
+
+ /**
+ * @param bool $bool
+ * @return static
+ */
+ public function withOrdered(bool $bool = true)
+ {
+ $list = array_keys(array_filter($this->call('isOrdered', [$bool])));
+
+ return $this->select($list);
+ }
+
+ /**
+ * @param bool $bool
+ * @return static
+ */
+ public function withModules(bool $bool = true)
+ {
+ $list = array_keys(array_filter($this->call('isModule', [$bool])));
+
+ return $this->select($list);
+ }
+
+ /**
+ * @param bool $bool
+ * @return static
+ */
+ public function withPages(bool $bool = true)
+ {
+ $list = array_keys(array_filter($this->call('isPage', [$bool])));
+
+ return $this->select($list);
+ }
+
+ /**
+ * @param bool $bool
+ * @param string|null $languageCode
+ * @param bool|null $fallback
+ * @return static
+ */
+ public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
+ {
+ $list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback])));
+
+ return $bool ? $this->select($list) : $this->unselect($list);
+ }
+
+ /**
+ * @param string|null $languageCode
+ * @param bool|null $fallback
+ * @return PageIndex
+ */
+ public function withTranslated(string $languageCode = null, bool $fallback = null)
+ {
+ return $this->getIndex()->withTranslated($languageCode, $fallback);
+ }
+
+ /**
+ * Filter pages by given filters.
+ *
+ * - search: string
+ * - page_type: string|string[]
+ * - modular: bool
+ * - visible: bool
+ * - routable: bool
+ * - published: bool
+ * - page: bool
+ * - translated: bool
+ *
+ * @param array $filters
+ * @param bool $recursive
+ * @return static
+ */
+ public function filterBy(array $filters, bool $recursive = false)
+ {
+ $list = array_keys(array_filter($this->call('filterBy', [$filters, $recursive])));
+
+ return $this->select($list);
+ }
+
+ /**
+ * Get the extended version of this Collection with each page keyed by route
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function toExtendedArray(): array
+ {
+ $entries = [];
+ foreach ($this as $key => $object) {
+ if ($object) {
+ $entries[$object->route()] = $object->toArray();
+ }
+ }
+
+ return $entries;
+ }
+
+ /**
+ * @param array $options
+ * @return array
+ */
+ public function getLevelListing(array $options): array
+ {
+ /** @var PageIndex $index */
+ $index = $this->getIndex();
+
+ return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : [];
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
new file mode 100644
index 00000000..f02a6486
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
@@ -0,0 +1,1163 @@
+
+ *
+ * @method PageIndex withModules(bool $bool = true)
+ * @method PageIndex withPages(bool $bool = true)
+ * @method PageIndex withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
+ */
+class PageIndex extends FlexPageIndex implements PageCollectionInterface
+{
+ use FlexGravTrait;
+ use FlexIndexTrait;
+
+ public const VERSION = parent::VERSION . '.5';
+ public const ORDER_LIST_REGEX = '/(\/\d+)\.[^\/]+/u';
+ public const PAGE_ROUTE_REGEX = '/\/\d+\./u';
+
+ /** @var PageObject|array */
+ protected $_root;
+ /** @var array|null */
+ protected $_params;
+
+ /**
+ * @param array $entries
+ * @param FlexDirectory|null $directory
+ */
+ public function __construct(array $entries = [], FlexDirectory $directory = null)
+ {
+ // Remove root if it's taken.
+ if (isset($entries[''])) {
+ $this->_root = $entries[''];
+ unset($entries['']);
+ }
+
+ parent::__construct($entries, $directory);
+ }
+
+ /**
+ * @param FlexStorageInterface $storage
+ * @return array
+ */
+ public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
+ {
+ // Load saved index.
+ $index = static::loadIndex($storage);
+
+ $version = $index['version'] ?? 0;
+ $force = static::VERSION !== $version;
+
+ // TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found.
+ //$timestamp = $index['timestamp'] ?? 0;
+ //if (!$force && $timestamp && $timestamp > time() - 1) {
+ // return $index['index'];
+ //}
+
+ // Load up to date index.
+ $entries = parent::loadEntriesFromStorage($storage);
+
+ return static::updateIndexFile($storage, $index['index'], $entries, ['include_missing' => true, 'force_update' => $force]);
+ }
+
+ /**
+ * @param string $key
+ * @return PageObject|null
+ */
+ public function get($key)
+ {
+ if (mb_strpos($key, '|') !== false) {
+ [$key, $params] = explode('|', $key, 2);
+ }
+
+ $element = parent::get($key);
+ if (null === $element) {
+ return null;
+ }
+
+ if (isset($params)) {
+ $element = $element->getTranslation(ltrim($params, '.'));
+ }
+
+ return $element;
+ }
+
+ /**
+ * @return PageObject
+ */
+ public function getRoot()
+ {
+ $root = $this->_root;
+ if (is_array($root)) {
+ $directory = $this->getFlexDirectory();
+ $storage = $directory->getStorage();
+
+ $defaults = [
+ 'header' => [
+ 'routable' => false,
+ 'permissions' => [
+ 'inherit' => false
+ ]
+ ]
+ ];
+
+ $row = $storage->readRows(['' => null])[''] ?? null;
+ if (null !== $row) {
+ if (isset($row['__ERROR'])) {
+ /** @var Debugger $debugger */
+ $debugger = Grav::instance()['debugger'];
+ $message = sprintf('Flex Pages: root page is broken in storage: %s', $row['__ERROR']);
+
+ $debugger->addException(new RuntimeException($message));
+ $debugger->addMessage($message, 'error');
+
+ $row = ['__META' => $root];
+ }
+
+ } else {
+ $row = ['__META' => $root];
+ }
+
+ $row = array_merge_recursive($defaults, $row);
+
+ /** @var PageObject $root */
+ $root = $this->getFlexDirectory()->createObject($row, '/', false);
+ $root->name('root.md');
+ $root->root(true);
+
+ $this->_root = $root;
+ }
+
+ return $root;
+ }
+
+ /**
+ * @param string|null $languageCode
+ * @param bool|null $fallback
+ * @return PageIndex
+ */
+ public function withTranslated(string $languageCode = null, bool $fallback = null)
+ {
+ if (null === $languageCode) {
+ return $this;
+ }
+
+ $entries = $this->translateEntries($this->getEntries(), $languageCode, $fallback);
+ $params = ['language' => $languageCode, 'language_fallback' => $fallback] + $this->getParams();
+
+ return $this->createFrom($entries)->setParams($params);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getLanguage(): ?string
+ {
+ return $this->_params['language'] ?? null;
+ }
+
+ /**
+ * Get the collection params
+ *
+ * @return array
+ */
+ public function getParams(): array
+ {
+ return $this->_params ?? [];
+ }
+
+ /**
+ * Get the collection param
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function getParam(string $name)
+ {
+ return $this->_params[$name] ?? null;
+ }
+
+ /**
+ * Set parameters to the Collection
+ *
+ * @param array $params
+ * @return $this
+ */
+ public function setParams(array $params)
+ {
+ $this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
+
+ return $this;
+ }
+
+ /**
+ * Set a parameter to the Collection
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return $this
+ */
+ public function setParam(string $name, $value)
+ {
+ $this->_params[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get the collection params
+ *
+ * @return array
+ */
+ public function params(): array
+ {
+ return $this->getParams();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @see FlexCollectionInterface::getCacheKey()
+ */
+ public function getCacheKey(): string
+ {
+ return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->getKeyField() . $this->getLanguage());
+ }
+
+ /**
+ * Filter pages by given filters.
+ *
+ * - search: string
+ * - page_type: string|string[]
+ * - modular: bool
+ * - visible: bool
+ * - routable: bool
+ * - published: bool
+ * - page: bool
+ * - translated: bool
+ *
+ * @param array $filters
+ * @param bool $recursive
+ * @return static
+ */
+ public function filterBy(array $filters, bool $recursive = false)
+ {
+ if (!$filters) {
+ return $this;
+ }
+
+ if ($recursive) {
+ return $this->__call('filterBy', [$filters, true]);
+ }
+
+ $list = [];
+ $index = $this;
+ foreach ($filters as $key => $value) {
+ switch ($key) {
+ case 'search':
+ $index = $index->search((string)$value);
+ break;
+ case 'page_type':
+ if (!is_array($value)) {
+ $value = is_string($value) && $value !== '' ? explode(',', $value) : [];
+ }
+ $index = $index->ofOneOfTheseTypes($value);
+ break;
+ case 'routable':
+ $index = $index->withRoutable((bool)$value);
+ break;
+ case 'published':
+ $index = $index->withPublished((bool)$value);
+ break;
+ case 'visible':
+ $index = $index->withVisible((bool)$value);
+ break;
+ case 'module':
+ $index = $index->withModules((bool)$value);
+ break;
+ case 'page':
+ $index = $index->withPages((bool)$value);
+ break;
+ case 'folder':
+ $index = $index->withPages(!$value);
+ break;
+ case 'translated':
+ $index = $index->withTranslation((bool)$value);
+ break;
+ default:
+ $list[$key] = $value;
+ }
+ }
+
+ return $list ? $index->filterByParent($list) : $index;
+ }
+
+ /**
+ * @param array $filters
+ * @return static
+ */
+ protected function filterByParent(array $filters)
+ {
+ /** @var static $index */
+ $index = parent::filterBy($filters);
+
+ return $index;
+ }
+
+ /**
+ * @param array $options
+ * @return array
+ */
+ public function getLevelListing(array $options): array
+ {
+ // Undocumented B/C
+ $order = $options['order'] ?? 'asc';
+ if ($order === SORT_ASC) {
+ $options['order'] = 'asc';
+ } elseif ($order === SORT_DESC) {
+ $options['order'] = 'desc';
+ }
+
+ $options += [
+ 'field' => null,
+ 'route' => null,
+ 'leaf_route' => null,
+ 'sortby' => null,
+ 'order' => 'asc',
+ 'lang' => null,
+ 'filters' => [],
+ ];
+
+ $options['filters'] += [
+ 'type' => ['root', 'dir'],
+ ];
+
+ $key = 'page.idx.lev.' . sha1(json_encode($options, JSON_THROW_ON_ERROR) . $this->getCacheKey());
+ $checksum = $this->getCacheChecksum();
+
+ $cache = $this->getCache('object');
+
+ /** @var Debugger $debugger */
+ $debugger = Grav::instance()['debugger'];
+
+ $result = null;
+ try {
+ $cached = $cache->get($key);
+ $test = $cached[0] ?? null;
+ $result = $test === $checksum ? ($cached[1] ?? null) : null;
+ } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
+ $debugger->addException($e);
+ }
+
+ try {
+ if (null === $result) {
+ $result = $this->getLevelListingRecurse($options);
+ $cache->set($key, [$checksum, $result]);
+ }
+ } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
+ $debugger->addException($e);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param array $entries
+ * @param string|null $keyField
+ * @return static
+ */
+ protected function createFrom(array $entries, string $keyField = null)
+ {
+ /** @var static $index */
+ $index = parent::createFrom($entries, $keyField);
+ $index->_root = $this->getRoot();
+
+ return $index;
+ }
+
+ /**
+ * @param array $entries
+ * @param string $lang
+ * @param bool|null $fallback
+ * @return array
+ */
+ protected function translateEntries(array $entries, string $lang, bool $fallback = null): array
+ {
+ $languages = $this->getFallbackLanguages($lang, $fallback);
+ foreach ($entries as $key => &$entry) {
+ // Find out which version of the page we should load.
+ $translations = $this->getLanguageTemplates((string)$key);
+ if (!$translations) {
+ // No translations found, is this a folder?
+ continue;
+ }
+
+ // Find a translation.
+ $template = null;
+ foreach ($languages as $code) {
+ if (isset($translations[$code])) {
+ $template = $translations[$code];
+ break;
+ }
+ }
+
+ // We couldn't find a translation, remove entry from the list.
+ if (!isset($code, $template)) {
+ unset($entries['key']);
+ continue;
+ }
+
+ // Get the main key without template and langauge.
+ [$main_key,] = explode('|', $entry['storage_key'] . '|', 2);
+
+ // Update storage key and language.
+ $entry['storage_key'] = $main_key . '|' . $template . '.' . $code;
+ $entry['lang'] = $code;
+ }
+ unset($entry);
+
+ return $entries;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getLanguageTemplates(string $key): array
+ {
+ $meta = $this->getMetaData($key);
+ $template = $meta['template'] ?? 'folder';
+ $translations = $meta['markdown'] ?? [];
+ $list = [];
+ foreach ($translations as $code => $search) {
+ if (isset($search[$template])) {
+ // Use main template if possible.
+ $list[$code] = $template;
+ } elseif (!empty($search)) {
+ // Fall back to first matching template.
+ $list[$code] = key($search);
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * @param string|null $languageCode
+ * @param bool|null $fallback
+ * @return array
+ */
+ protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
+ {
+ $fallback = $fallback ?? true;
+ if (!$fallback && null !== $languageCode) {
+ return [$languageCode];
+ }
+
+ $grav = Grav::instance();
+
+ /** @var Language $language */
+ $language = $grav['language'];
+ $languageCode = $languageCode ?? '';
+ if ($languageCode === '' && $fallback) {
+ return $language->getFallbackLanguages(null, true);
+ }
+
+ return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode];
+ }
+
+ /**
+ * @param array $options
+ * @return array
+ */
+ protected function getLevelListingRecurse(array $options): array
+ {
+ $filters = $options['filters'] ?? [];
+ $field = $options['field'];
+ $route = $options['route'];
+ $leaf_route = $options['leaf_route'];
+ $sortby = $options['sortby'];
+ $order = $options['order'];
+ $language = $options['lang'];
+
+ $status = 'error';
+ $msg = null;
+ $response = [];
+ $children = null;
+ $sub_route = null;
+ $extra = null;
+
+ // Handle leaf_route
+ $leaf = null;
+ if ($leaf_route && $route !== $leaf_route) {
+ $nodes = explode('/', $leaf_route);
+ $sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++));
+ $options['route'] = $sub_route;
+
+ [$status,,$leaf,$extra] = $this->getLevelListingRecurse($options);
+ }
+
+ // Handle no route, assume page tree root
+ if (!$route) {
+ $page = $this->getRoot();
+ } else {
+ $page = $this->get(trim($route, '/'));
+ }
+ $path = $page ? $page->path() : null;
+
+ if ($field) {
+ // Get forced filters from the field.
+ $blueprint = $page ? $page->getBlueprint() : $this->getFlexDirectory()->getBlueprint();
+ $settings = $blueprint->schema()->getProperty($field);
+ $filters = array_merge([], $filters, $settings['filters'] ?? []);
+ }
+
+ // Clean up filter.
+ $filter_type = (array)($filters['type'] ?? []);
+ unset($filters['type']);
+ $filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; });
+
+ if ($page) {
+ if ($page->root() && (!$filter_type || in_array('root', $filter_type, true))) {
+ if ($field) {
+ $response[] = [
+ 'name' => '',
+ 'value' => '/',
+ 'item-key' => '',
+ 'filename' => '.',
+ 'extension' => '',
+ 'type' => 'root',
+ 'modified' => $page->modified(),
+ 'size' => 0,
+ 'symlink' => false,
+ 'has-children' => false
+ ];
+ } else {
+ $response[] = [
+ 'item-key' => '-root-',
+ 'icon' => 'root',
+ 'title' => 'Root', // FIXME
+ 'route' => [
+ 'display' => '<root>', // FIXME
+ 'raw' => '_root',
+ ],
+ 'modified' => $page->modified(),
+ 'extras' => [
+ 'template' => $page->template(),
+ //'lang' => null,
+ //'translated' => null,
+ 'langs' => [],
+ 'published' => false,
+ 'visible' => false,
+ 'routable' => false,
+ 'tags' => ['root', 'non-routable'],
+ 'actions' => ['edit'], // FIXME
+ ]
+ ];
+ }
+ }
+
+ $status = 'success';
+ $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
+
+ /** @var PageIndex $children */
+ $children = $page->children()->getIndex();
+ $selectedChildren = $children->filterBy($filters, true);
+
+ /** @var Header $header */
+ $header = $page->header();
+
+ if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) {
+ // Use custom sorting by page header.
+ $sortby = $orderby;
+ $order = $header->get('content.order.dir', $order);
+ $custom = $header->get('content.order.custom');
+ }
+
+ if ($sortby) {
+ // Sort children.
+ $selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null);
+ }
+
+ /** @var UserInterface|null $user */
+ $user = Grav::instance()['user'] ?? null;
+
+ /** @var PageObject $child */
+ foreach ($selectedChildren as $child) {
+ $selected = $child->path() === $extra;
+ $includeChildren = is_array($leaf) && !empty($leaf) && $selected;
+ if ($field) {
+ $child_count = count($child->children());
+ $payload = [
+ 'name' => $child->menu(),
+ 'value' => $child->rawRoute(),
+ 'item-key' => basename($child->rawRoute() ?? ''),
+ 'filename' => $child->folder(),
+ 'extension' => $child->extension(),
+ 'type' => 'dir',
+ 'modified' => $child->modified(),
+ 'size' => $child_count,
+ 'symlink' => false,
+ 'has-children' => $child_count > 0
+ ];
+ } else {
+ $lang = $child->findTranslation($language) ?? 'n/a';
+ /** @var PageObject $child */
+ $child = $child->getTranslation($language) ?? $child;
+
+ // TODO: all these features are independent from each other, we cannot just have one icon/color to catch all.
+ // TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc..
+ if ($child->home()) {
+ $icon = 'home';
+ } elseif ($child->isModule()) {
+ $icon = 'modular';
+ } elseif ($child->visible()) {
+ $icon = 'visible';
+ } elseif ($child->isPage()) {
+ $icon = 'page';
+ } else {
+ // TODO: add support
+ $icon = 'folder';
+ }
+ $tags = [
+ $child->published() ? 'published' : 'non-published',
+ $child->visible() ? 'visible' : 'non-visible',
+ $child->routable() ? 'routable' : 'non-routable'
+ ];
+ $extras = [
+ 'template' => $child->template(),
+ 'lang' => $lang ?: null,
+ 'translated' => $lang ? $child->hasTranslation($language, false) : null,
+ 'langs' => $child->getAllLanguages(true) ?: null,
+ 'published' => $child->published(),
+ 'published_date' => $this->jsDate($child->publishDate()),
+ 'unpublished_date' => $this->jsDate($child->unpublishDate()),
+ 'visible' => $child->visible(),
+ 'routable' => $child->routable(),
+ 'tags' => $tags,
+ 'actions' => $this->getListingActions($child, $user),
+ ];
+ $extras = array_filter($extras, static function ($v) {
+ return $v !== null;
+ });
+ $tmp = $child->children()->getIndex();
+ $child_count = $tmp->count();
+ $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
+ $route = $child->getRoute();
+ $route = $route ? ($route->toString(false) ?: '/') : '';
+ $payload = [
+ 'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
+ 'icon' => $icon,
+ 'title' => htmlspecialchars($child->menu()),
+ 'route' => [
+ 'display' => htmlspecialchars($route) ?: null,
+ 'raw' => htmlspecialchars($child->rawRoute()),
+ ],
+ 'modified' => $this->jsDate($child->modified()),
+ 'child_count' => $child_count ?: null,
+ 'count' => $count ?? null,
+ 'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null,
+ 'extras' => $extras
+ ];
+ $payload = array_filter($payload, static function ($v) {
+ return $v !== null;
+ });
+ }
+
+ // Add children if any
+ if ($includeChildren) {
+ $payload['children'] = array_values($leaf);
+ }
+
+ $response[] = $payload;
+ }
+ } else {
+ $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
+ }
+
+ if ($field) {
+ $temp_array = [];
+ foreach ($response as $index => $item) {
+ $temp_array[$item['type']][$index] = $item;
+ }
+
+ $sorted = Utils::sortArrayByArray($temp_array, $filter_type);
+ $response = Utils::arrayFlatten($sorted);
+ }
+
+ return [$status, $msg ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED', $response, $path];
+ }
+
+ /**
+ * @param PageObject $object
+ * @param UserInterface $user
+ * @return array
+ */
+ protected function getListingActions(PageObject $object, UserInterface $user): array
+ {
+ $actions = [];
+ if ($object->isAuthorized('read', null, $user)) {
+ $actions[] = 'preview';
+ $actions[] = 'edit';
+ }
+ if ($object->isAuthorized('update', null, $user)) {
+ $actions[] = 'copy';
+ $actions[] = 'move';
+ }
+ if ($object->isAuthorized('delete', null, $user)) {
+ $actions[] = 'delete';
+ }
+
+ return $actions;
+ }
+
+ /**
+ * @param FlexStorageInterface $storage
+ * @return CompiledJsonFile|CompiledYamlFile|null
+ */
+ protected static function getIndexFile(FlexStorageInterface $storage)
+ {
+ if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) {
+ return null;
+ }
+
+ // Load saved index file.
+ $grav = Grav::instance();
+ $locator = $grav['locator'];
+
+ $filename = $locator->findResource('user-data://flex/indexes/pages.json', true, true);
+
+ return CompiledJsonFile::instance($filename);
+ }
+
+ /**
+ * @param int|null $timestamp
+ * @return string|null
+ */
+ private function jsDate(int $timestamp = null): ?string
+ {
+ if (!$timestamp) {
+ return null;
+ }
+
+ $config = Grav::instance()['config'];
+ $dateFormat = $config->get('system.pages.dateformat.long');
+
+ return date($dateFormat, $timestamp) ?: null;
+ }
+
+ /**
+ * Add a single page to a collection
+ *
+ * @param PageInterface $page
+ * @return PageCollection
+ */
+ public function addPage(PageInterface $page)
+ {
+ return $this->getCollection()->addPage($page);
+ }
+
+ /**
+ *
+ * Create a copy of this collection
+ *
+ * @return static
+ */
+ public function copy()
+ {
+ return clone $this;
+ }
+
+ /**
+ *
+ * Merge another collection with the current collection
+ *
+ * @param PageCollectionInterface $collection
+ * @return PageCollection
+ */
+ public function merge(PageCollectionInterface $collection)
+ {
+ return $this->getCollection()->merge($collection);
+ }
+
+
+ /**
+ * Intersect another collection with the current collection
+ *
+ * @param PageCollectionInterface $collection
+ * @return PageCollection
+ */
+ public function intersect(PageCollectionInterface $collection)
+ {
+ return $this->getCollection()->intersect($collection);
+ }
+
+ /**
+ * Split collection into array of smaller collections.
+ *
+ * @param int $size
+ * @return PageCollection[]
+ */
+ public function batch($size)
+ {
+ return $this->getCollection()->batch($size);
+ }
+
+ /**
+ * Remove item from the list.
+ *
+ * @param string $key
+ * @return PageObject|null
+ * @throws InvalidArgumentException
+ */
+ public function remove($key)
+ {
+ return $this->getCollection()->remove($key);
+ }
+
+ /**
+ * Reorder collection.
+ *
+ * @param string $by
+ * @param string $dir
+ * @param array $manual
+ * @param string $sort_flags
+ * @return static
+ */
+ public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
+ {
+ /** @var PageCollectionInterface $collection */
+ $collection = $this->__call('order', [$by, $dir, $manual, $sort_flags]);
+
+ return $collection;
+ }
+
+ /**
+ * Check to see if this item is the first in the collection.
+ *
+ * @param string $path
+ * @return bool True if item is first.
+ */
+ public function isFirst($path): bool
+ {
+ /** @var bool $result */
+ $result = $this->__call('isFirst', [$path]);
+
+ return $result;
+
+ }
+
+ /**
+ * Check to see if this item is the last in the collection.
+ *
+ * @param string $path
+ * @return bool True if item is last.
+ */
+ public function isLast($path): bool
+ {
+ /** @var bool $result */
+ $result = $this->__call('isLast', [$path]);
+
+ return $result;
+ }
+
+ /**
+ * Gets the previous sibling based on current position.
+ *
+ * @param string $path
+ * @return PageObject|null The previous item.
+ */
+ public function prevSibling($path)
+ {
+ /** @var PageObject|null $result */
+ $result = $this->__call('prevSibling', [$path]);
+
+ return $result;
+ }
+
+ /**
+ * Gets the next sibling based on current position.
+ *
+ * @param string $path
+ * @return PageObject|null The next item.
+ */
+ public function nextSibling($path)
+ {
+ /** @var PageObject|null $result */
+ $result = $this->__call('nextSibling', [$path]);
+
+ return $result;
+ }
+
+ /**
+ * Returns the adjacent sibling based on a direction.
+ *
+ * @param string $path
+ * @param int $direction either -1 or +1
+ * @return PageObject|false The sibling item.
+ */
+ public function adjacentSibling($path, $direction = 1)
+ {
+ /** @var PageObject|false $result */
+ $result = $this->__call('adjacentSibling', [$path, $direction]);
+
+ return $result;
+ }
+
+ /**
+ * Returns the item in the current position.
+ *
+ * @param string $path the path the item
+ * @return int|null The index of the current page, null if not found.
+ */
+ public function currentPosition($path): ?int
+ {
+ /** @var int|null $result */
+ $result = $this->__call('currentPosition', [$path]);
+
+ return $result;
+ }
+
+ /**
+ * Returns the items between a set of date ranges of either the page date field (default) or
+ * an arbitrary datetime page field where start date and end date are optional
+ * Dates must be passed in as text that strtotime() can process
+ * http://php.net/manual/en/function.strtotime.php
+ *
+ * @param string|null $startDate
+ * @param string|null $endDate
+ * @param string|null $field
+ * @return static
+ * @throws Exception
+ */
+ public function dateRange($startDate = null, $endDate = null, $field = null)
+ {
+ $collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
+
+ return $collection;
+ }
+
+ /**
+ * Mimicks Pages class.
+ *
+ * @return $this
+ * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
+ */
+ public function all()
+ {
+ return $this;
+ }
+
+ /**
+ * Creates new collection with only visible pages
+ *
+ * @return static The collection with only visible pages
+ */
+ public function visible()
+ {
+ $collection = $this->__call('visible', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only non-visible pages
+ *
+ * @return static The collection with only non-visible pages
+ */
+ public function nonVisible()
+ {
+ $collection = $this->__call('nonVisible', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only non-modular pages
+ *
+ * @return static The collection with only non-modular pages
+ */
+ public function pages()
+ {
+ $collection = $this->__call('pages', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only modular pages
+ *
+ * @return static The collection with only modular pages
+ */
+ public function modules()
+ {
+ $collection = $this->__call('modules', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only modular pages
+ *
+ * @return static The collection with only modular pages
+ */
+ public function modular()
+ {
+ return $this->modules();
+ }
+
+ /**
+ * Creates new collection with only non-modular pages
+ *
+ * @return static The collection with only non-modular pages
+ */
+ public function nonModular()
+ {
+ return $this->pages();
+ }
+
+ /**
+ * Creates new collection with only published pages
+ *
+ * @return static The collection with only published pages
+ */
+ public function published()
+ {
+ $collection = $this->__call('published', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only non-published pages
+ *
+ * @return static The collection with only non-published pages
+ */
+ public function nonPublished()
+ {
+ $collection = $this->__call('nonPublished', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only routable pages
+ *
+ * @return static The collection with only routable pages
+ */
+ public function routable()
+ {
+ $collection = $this->__call('routable', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only non-routable pages
+ *
+ * @return static The collection with only non-routable pages
+ */
+ public function nonRoutable()
+ {
+ $collection = $this->__call('nonRoutable', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only pages of the specified type
+ *
+ * @param string $type
+ * @return static The collection
+ */
+ public function ofType($type)
+ {
+ $collection = $this->__call('ofType', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only pages of one of the specified types
+ *
+ * @param string[] $types
+ * @return static The collection
+ */
+ public function ofOneOfTheseTypes($types)
+ {
+ $collection = $this->__call('ofOneOfTheseTypes', []);
+
+ return $collection;
+ }
+
+ /**
+ * Creates new collection with only pages of one of the specified access levels
+ *
+ * @param array $accessLevels
+ * @return static The collection
+ */
+ public function ofOneOfTheseAccessLevels($accessLevels)
+ {
+ $collection = $this->__call('ofOneOfTheseAccessLevels', []);
+
+ return $collection;
+ }
+
+ /**
+ * Converts collection into an array.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->getCollection()->toArray();
+ }
+
+ /**
+ * Get the extended version of this Collection with each page keyed by route
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function toExtendedArray()
+ {
+ return $this->getCollection()->toExtendedArray();
+ }
+
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
new file mode 100644
index 00000000..4c83f959
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
@@ -0,0 +1,730 @@
+ true,
+ 'full_order' => true,
+ 'filterBy' => true,
+ 'translated' => false,
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * @return void
+ */
+ public function initialize(): void
+ {
+ if (!$this->_initialized) {
+ Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
+ $this->_initialized = true;
+ }
+ }
+
+ public function translated(): bool
+ {
+ return $this->translatedLanguages(true) ? true : false;
+ }
+
+ /**
+ * @param string|array $query
+ * @return Route|null
+ */
+ public function getRoute($query = []): ?Route
+ {
+ $path = $this->route();
+ if (null === $path) {
+ return null;
+ }
+
+ $route = RouteFactory::createFromString($path);
+ if ($lang = $route->getLanguage()) {
+ $grav = Grav::instance();
+ if (!$grav['config']->get('system.languages.include_default_lang')) {
+ /** @var Language $language */
+ $language = $grav['language'];
+ if ($lang === $language->getDefault()) {
+ $route = $route->withLanguage('');
+ }
+ }
+ }
+ if (is_array($query)) {
+ foreach ($query as $key => $value) {
+ $route = $route->withQueryParam($key, $value);
+ }
+ } else {
+ $route = $route->withAddedPath($query);
+ }
+
+ return $route;
+ }
+
+ /**
+ * @inheritdoc PageInterface
+ */
+ public function getFormValue(string $name, $default = null, string $separator = null)
+ {
+ $test = new stdClass();
+
+ $value = $this->pageContentValue($name, $test);
+ if ($value !== $test) {
+ return $value;
+ }
+
+ switch ($name) {
+ case 'name':
+ // TODO: this should not be template!
+ return $this->getProperty('template');
+ case 'route':
+ $filesystem = Filesystem::getInstance(false);
+ $key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/');
+ return $key !== '/' ? $key : null;
+ case 'full_route':
+ return $this->hasKey() ? '/' . $this->getKey() : '';
+ case 'full_order':
+ return $this->full_order();
+ case 'lang':
+ return $this->getLanguage() ?? '';
+ case 'translations':
+ return $this->getLanguages();
+ }
+
+ return parent::getFormValue($name, $default, $separator);
+ }
+
+ /**
+ * {@inheritdoc}
+ * @see FlexObjectInterface::getCacheKey()
+ */
+ public function getCacheKey(): string
+ {
+ $cacheKey = parent::getCacheKey();
+ if ($cacheKey) {
+ /** @var Language $language */
+ $language = Grav::instance()['language'];
+ $cacheKey .= '_' . $language->getActive();
+ }
+
+ return $cacheKey;
+ }
+
+ /**
+ * @param array $variables
+ * @return array
+ */
+ protected function onBeforeSave(array $variables)
+ {
+ $reorder = $variables[0] ?? true;
+
+ $meta = $this->getMetaData();
+ if (($meta['copy'] ?? false) === true) {
+ $this->folder = $this->getKey();
+ }
+
+ // Figure out storage path to the new route.
+ $parentKey = $this->getProperty('parent_key');
+ if ($parentKey !== '') {
+ $parentRoute = $this->getProperty('route');
+
+ // Root page cannot be moved.
+ if ($this->root()) {
+ throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute));
+ }
+
+ // Make sure page isn't being moved under itself.
+ $key = $this->getStorageKey();
+
+ /** @var PageObject|null $parent */
+ $parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null;
+ if (!$parent) {
+ // Page cannot be moved to non-existing location.
+ throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute));
+ }
+
+ // TODO: make sure that the page doesn't exist yet if moved/copied.
+ }
+
+ if ($reorder === true && !$this->root()) {
+ $reorder = $this->_reorder;
+ }
+
+ // Force automatic reorder if item is supposed to be added to the last.
+ if (!is_array($reorder) && (int)$this->order() >= 999999) {
+ $reorder = [];
+ }
+
+ // Reorder siblings.
+ $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
+
+ $data = $this->prepareStorage();
+ unset($data['header']);
+
+ foreach ($siblings as $sibling) {
+ $data = $sibling->prepareStorage();
+ unset($data['header']);
+ }
+
+ return ['reorder' => $reorder, 'siblings' => $siblings];
+ }
+
+ /**
+ * @param array $variables
+ * @return array
+ */
+ protected function onSave(array $variables): array
+ {
+ /** @var PageCollection $siblings */
+ $siblings = $variables['siblings'];
+ foreach ($siblings as $sibling) {
+ $sibling->save(false);
+ }
+
+ return $variables;
+ }
+
+ /**
+ * @param array $variables
+ */
+ protected function onAfterSave(array $variables): void
+ {
+ $this->getFlexDirectory()->reloadIndex();
+ }
+
+ /**
+ * @param UserInterface|null $user
+ */
+ public function check(UserInterface $user = null): void
+ {
+ parent::check($user);
+
+ if ($user && $this->isMoved()) {
+ $parentKey = $this->getProperty('parent_key');
+
+ /** @var PageObject|null $parent */
+ $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
+ if (!$parent || !$parent->isAuthorized('create', null, $user)) {
+ throw new \RuntimeException('Forbidden', 403);
+ }
+ }
+ }
+
+ /**
+ * @param array|bool $reorder
+ * @return FlexObject|FlexObjectInterface
+ */
+ public function save($reorder = true)
+ {
+ $variables = $this->onBeforeSave(func_get_args());
+
+ // Backwards compatibility with older plugins.
+ $fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
+ $grav = $this->getContainer();
+ if ($fireEvents) {
+ $self = $this;
+ $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
+ if ($self !== $this) {
+ throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.');
+ }
+ }
+
+ /** @var static $instance */
+ $instance = parent::save();
+ $variables = $this->onSave($variables);
+
+ $this->onAfterSave($variables);
+
+ // Backwards compatibility with older plugins.
+ if ($fireEvents) {
+ $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
+ }
+
+ // Reset original after save events have all been called.
+ $this->_originalObject = null;
+
+ return $instance;
+ }
+
+ /**
+ * @return PageObject
+ */
+ public function delete()
+ {
+ $result = parent::delete();
+
+ // Backwards compatibility with older plugins.
+ $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
+ if ($fireEvents) {
+ $this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this]));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Prepare move page to new location. Moves also everything that's under the current page.
+ *
+ * You need to call $this->save() in order to perform the move.
+ *
+ * @param PageInterface $parent New parent page.
+ * @return $this
+ */
+ public function move(PageInterface $parent)
+ {
+ if (!$parent instanceof FlexObjectInterface) {
+ throw new RuntimeException('Failed: Parent is not Flex Object');
+ }
+
+ $this->_reorder = [];
+ $this->setProperty('parent_key', $parent->getStorageKey());
+ $this->storeOriginal();
+
+ return $this;
+ }
+
+ /**
+ * @param UserInterface $user
+ * @param string $action
+ * @param string $scope
+ * @param bool $isMe
+ * @return bool|null
+ */
+ protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
+ {
+ // Special case: creating a new page means checking parent for its permissions.
+ if ($action === 'create' && !$this->exists()) {
+ $parent = $this->parent();
+ if ($parent && method_exists($parent, 'isAuthorized')) {
+ return $parent->isAuthorized($action, $scope, $user);
+ }
+
+ return false;
+ }
+
+ return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isMoved(): bool
+ {
+ $storageKey = $this->getMasterKey();
+ $filesystem = Filesystem::getInstance(false);
+ $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
+ $newParentKey = $this->getProperty('parent_key');
+
+ return $this->exists() && $oldParentKey !== $newParentKey;
+ }
+
+ /**
+ * @param array $ordering
+ * @return PageCollection|null
+ */
+ protected function reorderSiblings(array $ordering)
+ {
+ $storageKey = $this->getMasterKey();
+ $isMoved = $this->isMoved();
+ $order = !$isMoved ? $this->order() : false;
+ if ($order !== false) {
+ $order = (int)$order;
+ }
+
+ $parent = $this->parent();
+ if (!$parent) {
+ throw new RuntimeException('Cannot reorder a page which has no parent');
+ }
+
+ /** @var PageCollection $siblings */
+ $siblings = $parent->children();
+ $siblings = $siblings->getCollection()->withOrdered();
+
+ // Handle special case where ordering isn't given.
+ if ($ordering === []) {
+ if ($order >= 999999) {
+ // Set ordering to point to be the last item, ignoring the object itself.
+ $order = 0;
+ foreach ($siblings as $sibling) {
+ if ($sibling->getKey() !== $this->getKey()) {
+ $order = max($order, (int)$sibling->order());
+ }
+ }
+ $this->order($order + 1);
+ }
+
+ // Do not change sibling ordering.
+ return null;
+ }
+
+ $siblings = $siblings->orderBy(['order' => 'ASC']);
+
+ if ($storageKey !== null) {
+ if ($order !== false) {
+ // Add current page back to the list if it's ordered.
+ $siblings->set($storageKey, $this);
+ } else {
+ // Remove old copy of the current page from the siblings list.
+ $siblings->remove($storageKey);
+ }
+ }
+
+ // Add missing siblings into the end of the list, keeping the previous ordering between them.
+ foreach ($siblings as $sibling) {
+ $folder = (string)$sibling->getProperty('folder');
+ $basename = preg_replace('|^\d+\.|', '', $folder);
+ if (!in_array($basename, $ordering, true)) {
+ $ordering[] = $basename;
+ }
+ }
+
+ // Reorder.
+ $ordering = array_flip(array_values($ordering));
+ $count = count($ordering);
+ foreach ($siblings as $sibling) {
+ $folder = (string)$sibling->getProperty('folder');
+ $basename = preg_replace('|^\d+\.|', '', $folder);
+ $newOrder = $ordering[$basename] ?? null;
+ $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
+ $sibling->order($newOrder);
+ }
+
+ $siblings = $siblings->orderBy(['order' => 'ASC']);
+ $siblings->removeElement($this);
+
+ // If menu item was moved, just make it to be the last in order.
+ if ($isMoved && $this->order() !== false) {
+ $parentKey = $this->getProperty('parent_key');
+ if ($parentKey === '') {
+ $newParent = $this->getFlexDirectory()->getIndex()->getRoot();
+ } else {
+ $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
+ if (!$newParent instanceof PageInterface) {
+ throw new RuntimeException("New parent page '{$parentKey}' not found.");
+ }
+ }
+ /** @var PageCollection $newSiblings */
+ $newSiblings = $newParent->children();
+ $newSiblings = $newSiblings->getCollection()->withOrdered();
+ $order = 0;
+ foreach ($newSiblings as $sibling) {
+ $order = max($order, (int)$sibling->order());
+ }
+ $this->order($order + 1);
+ }
+
+ return $siblings;
+ }
+
+ /**
+ * @return string
+ */
+ public function full_order(): string
+ {
+ $route = $this->path() . '/' . $this->folder();
+
+ return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route;
+ }
+
+ /**
+ * @param string $name
+ * @return Blueprint
+ */
+ protected function doGetBlueprint(string $name = ''): Blueprint
+ {
+ try {
+ // Make sure that pages has been initialized.
+ Pages::getTypes();
+
+ // TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here.
+ if ($name === 'raw') {
+ // Admin RAW mode.
+ if ($this->isAdminSite()) {
+ /** @var Admin $admin */
+ $admin = Grav::instance()['admin'];
+
+ $template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw');
+
+ return $admin->blueprints("admin/pages/{$template}");
+ }
+ }
+
+ $template = $this->getProperty('template') . ($name ? '.' . $name : '');
+
+ $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
+ } catch (RuntimeException $e) {
+ $template = 'default' . ($name ? '.' . $name : '');
+
+ $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
+ }
+
+ $isNew = $blueprint->get('initialized', false) === false;
+ if ($isNew === true && $name === '') {
+ // Support onBlueprintCreated event just like in Pages::blueprints($template)
+ $blueprint->set('initialized', true);
+ $blueprint->setFilename($template);
+
+ Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
+ }
+
+ return $blueprint;
+ }
+
+ /**
+ * @param array $options
+ * @return array
+ */
+ public function getLevelListing(array $options): array
+ {
+ $index = $this->getFlexDirectory()->getIndex();
+ if (!is_callable([$index, 'getLevelListing'])) {
+ return [];
+ }
+
+ // Deal with relative paths.
+ $initial = $options['initial'] ?? null;
+ $var = $initial ? 'leaf_route' : 'route';
+ $route = $options[$var] ?? '';
+ if ($route !== '' && !str_starts_with($route, '/')) {
+ $filesystem = Filesystem::getInstance();
+
+ $route = "/{$this->getKey()}/{$route}";
+ $route = $filesystem->normalize($route);
+
+ $options[$var] = $route;
+ }
+
+ [$status, $message, $response,] = $index->getLevelListing($options);
+
+ return [$status, $message, $response, $options[$var] ?? null];
+ }
+
+ /**
+ * Filter page (true/false) by given filters.
+ *
+ * - search: string
+ * - extension: string
+ * - module: bool
+ * - visible: bool
+ * - routable: bool
+ * - published: bool
+ * - page: bool
+ * - translated: bool
+ *
+ * @param array $filters
+ * @param bool $recursive
+ * @return bool
+ */
+ public function filterBy(array $filters, bool $recursive = false): bool
+ {
+ foreach ($filters as $key => $value) {
+ switch ($key) {
+ case 'search':
+ $matches = $this->search((string)$value) > 0.0;
+ break;
+ case 'page_type':
+ $types = $value ? explode(',', $value) : [];
+ $matches = in_array($this->template(), $types, true);
+ break;
+ case 'extension':
+ $matches = Utils::contains((string)$value, $this->extension());
+ break;
+ case 'routable':
+ $matches = $this->isRoutable() === (bool)$value;
+ break;
+ case 'published':
+ $matches = $this->isPublished() === (bool)$value;
+ break;
+ case 'visible':
+ $matches = $this->isVisible() === (bool)$value;
+ break;
+ case 'module':
+ $matches = $this->isModule() === (bool)$value;
+ break;
+ case 'page':
+ $matches = $this->isPage() === (bool)$value;
+ break;
+ case 'folder':
+ $matches = $this->isPage() === !$value;
+ break;
+ case 'translated':
+ $matches = $this->hasTranslation() === (bool)$value;
+ break;
+ default:
+ $matches = true;
+ break;
+ }
+
+ // If current filter does not match, we still may have match as a parent.
+ if ($matches === false) {
+ return $recursive && $this->children()->getIndex()->filterBy($filters, true)->count() > 0;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @see FlexObjectInterface::exists()
+ */
+ public function exists(): bool
+ {
+ return $this->root ?: parent::exists();
+ }
+
+ /**
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $list = parent::__debugInfo();
+
+ return $list + [
+ '_content_meta:private' => $this->getContentMeta(),
+ '_content:private' => $this->getRawContent()
+ ];
+ }
+
+ /**
+ * @param array $elements
+ * @param bool $extended
+ */
+ protected function filterElements(array &$elements, bool $extended = false): void
+ {
+ // Change parent page if needed.
+ if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) {
+ $elements['template'] = $elements['name'];
+
+ // Figure out storage path to the new route.
+ $parentKey = trim($elements['route'] ?? '', '/');
+ if ($parentKey !== '') {
+ /** @var PageObject|null $parent */
+ $parent = $this->getFlexDirectory()->getObject($parentKey);
+ $parentKey = $parent ? $parent->getStorageKey() : $parentKey;
+ }
+
+ $elements['parent_key'] = $parentKey;
+ }
+
+ // Deal with ordering=bool and order=page1,page2,page3.
+ if ($this->root()) {
+ // Root page doesn't have ordering.
+ unset($elements['ordering'], $elements['order']);
+ } elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
+ // Store ordering.
+ $ordering = $elements['order'] ?? null;
+ $this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
+
+ $order = false;
+ if ((bool)($elements['ordering'] ?? false)) {
+ $order = $this->order();
+ if ($order === false) {
+ $order = 999999;
+ }
+ }
+
+ $elements['order'] = $order;
+ }
+
+ parent::filterElements($elements, true);
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareStorage(): array
+ {
+ $meta = $this->getMetaData();
+ $oldLang = $meta['lang'] ?? '';
+ $newLang = $this->getProperty('lang') ?? '';
+
+ // Always clone the page to the new language.
+ if ($oldLang !== $newLang) {
+ $meta['clone'] = true;
+ }
+
+ // Make sure that certain elements are always sent to the storage layer.
+ $elements = [
+ '__META' => $meta,
+ 'storage_key' => $this->getStorageKey(),
+ 'parent_key' => $this->getProperty('parent_key'),
+ 'order' => $this->getProperty('order'),
+ 'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
+ 'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
+ 'lang' => $newLang
+ ] + parent::prepareStorage();
+
+ return $elements;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
new file mode 100644
index 00000000..ba7fa067
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php
@@ -0,0 +1,700 @@
+flags = FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::CURRENT_AS_FILEINFO
+ | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
+
+ $grav = Grav::instance();
+
+ $config = $grav['config'];
+ $this->ignore_hidden = (bool)$config->get('system.pages.ignore_hidden');
+ $this->ignore_files = (array)$config->get('system.pages.ignore_files');
+ $this->ignore_folders = (array)$config->get('system.pages.ignore_folders');
+ $this->include_default_lang_file_extension = (bool)$config->get('system.languages.include_default_lang_file_extension', true);
+ $this->recurse = (bool)($options['recurse'] ?? true);
+ $this->regex = '/(\.([\w\d_-]+))?\.md$/D';
+ }
+
+ /**
+ * @param string $key
+ * @param bool $variations
+ * @return array
+ */
+ public function parseKey(string $key, bool $variations = true): array
+ {
+ if (mb_strpos($key, '|') !== false) {
+ [$key, $params] = explode('|', $key, 2);
+ } else {
+ $params = '';
+ }
+ $key = ltrim($key, '/');
+
+ $keys = parent::parseKey($key, false) + ['params' => $params];
+
+ if ($variations) {
+ $keys += $this->parseParams($key, $params);
+ }
+
+ return $keys;
+ }
+
+ /**
+ * @param string $key
+ * @return string
+ */
+ public function readFrontmatter(string $key): string
+ {
+ $path = $this->getPathFromKey($key);
+ $file = $this->getFile($path);
+ try {
+ if ($file instanceof MarkdownFile) {
+ $frontmatter = $file->frontmatter();
+ } else {
+ $frontmatter = $file->raw();
+ }
+ } catch (RuntimeException $e) {
+ $frontmatter = 'ERROR: ' . $e->getMessage();
+ } finally {
+ $file->free();
+ unset($file);
+ }
+
+ return $frontmatter;
+ }
+
+ /**
+ * @param string $key
+ * @return string
+ */
+ public function readRaw(string $key): string
+ {
+ $path = $this->getPathFromKey($key);
+ $file = $this->getFile($path);
+ try {
+ $raw = $file->raw();
+ } catch (RuntimeException $e) {
+ $raw = 'ERROR: ' . $e->getMessage();
+ } finally {
+ $file->free();
+ unset($file);
+ }
+
+ return $raw;
+ }
+
+ /**
+ * @param array $keys
+ * @param bool $includeParams
+ * @return string
+ */
+ public function buildStorageKey(array $keys, bool $includeParams = true): string
+ {
+ $key = $keys['key'] ?? null;
+ if (null === $key) {
+ $key = $keys['parent_key'] ?? '';
+ if ($key !== '') {
+ $key .= '/';
+ }
+ $order = $keys['order'] ?? null;
+ $folder = $keys['folder'] ?? 'undefined';
+ $key .= is_numeric($order) ? sprintf('%02d.%s', $order, $folder) : $folder;
+ }
+
+ $params = $includeParams ? $this->buildStorageKeyParams($keys) : '';
+
+ return $params ? "{$key}|{$params}" : $key;
+ }
+
+ /**
+ * @param array $keys
+ * @return string
+ */
+ public function buildStorageKeyParams(array $keys): string
+ {
+ $params = $keys['template'] ?? '';
+ $language = $keys['lang'] ?? '';
+ if ($language) {
+ $params .= '.' . $language;
+ }
+
+ return $params;
+ }
+
+ /**
+ * @param array $keys
+ * @return string
+ */
+ public function buildFolder(array $keys): string
+ {
+ return $this->dataFolder . '/' . $this->buildStorageKey($keys, false);
+ }
+
+ /**
+ * @param array $keys
+ * @return string
+ */
+ public function buildFilename(array $keys): string
+ {
+ $file = $this->buildStorageKeyParams($keys);
+
+ // Template is optional; if it is missing, we need to have to load the object metadata.
+ if ($file && $file[0] === '.') {
+ $meta = $this->getObjectMeta($this->buildStorageKey($keys, false));
+ $file = ($meta['template'] ?? 'folder') . $file;
+ }
+
+ return $file . $this->dataExt;
+ }
+
+ /**
+ * @param array $keys
+ * @return string
+ */
+ public function buildFilepath(array $keys): string
+ {
+ $folder = $this->buildFolder($keys);
+ $filename = $this->buildFilename($keys);
+
+ return rtrim($folder, '/') !== $folder ? $folder . $filename : $folder . '/' . $filename;
+ }
+
+ /**
+ * @param array $row
+ * @param bool $setDefaultLang
+ * @return array
+ */
+ public function extractKeysFromRow(array $row, bool $setDefaultLang = true): array
+ {
+ $meta = $row['__META'] ?? null;
+ $storageKey = $row['storage_key'] ?? $meta['storage_key'] ?? '';
+ $keyMeta = $storageKey !== '' ? $this->extractKeysFromStorageKey($storageKey) : null;
+ $parentKey = $row['parent_key'] ?? $meta['parent_key'] ?? $keyMeta['parent_key'] ?? '';
+ $order = $row['order'] ?? $meta['order'] ?? $keyMeta['order'] ?? null;
+ $folder = $row['folder'] ?? $meta['folder'] ?? $keyMeta['folder'] ?? '';
+ $template = $row['template'] ?? $meta['template'] ?? $keyMeta['template'] ?? '';
+ $lang = $row['lang'] ?? $meta['lang'] ?? $keyMeta['lang'] ?? '';
+
+ // Handle default language, if it should be saved without language extension.
+ if ($setDefaultLang && empty($meta['markdown'][$lang])) {
+ $grav = Grav::instance();
+
+ /** @var Language $language */
+ $language = $grav['language'];
+ $default = $language->getDefault();
+ // Make sure that the default language file doesn't exist before overriding it.
+ if (empty($meta['markdown'][$default])) {
+ if ($this->include_default_lang_file_extension) {
+ if ($lang === '') {
+ $lang = $language->getDefault();
+ }
+ } elseif ($lang === $language->getDefault()) {
+ $lang = '';
+ }
+ }
+ }
+
+ $keys = [
+ 'key' => null,
+ 'params' => null,
+ 'parent_key' => $parentKey,
+ 'order' => is_numeric($order) ? (int)$order : null,
+ 'folder' => $folder,
+ 'template' => $template,
+ 'lang' => $lang
+ ];
+
+ $keys['key'] = $this->buildStorageKey($keys, false);
+ $keys['params'] = $this->buildStorageKeyParams($keys);
+
+ return $keys;
+ }
+
+ /**
+ * @param string $key
+ * @return array
+ */
+ public function extractKeysFromStorageKey(string $key): array
+ {
+ if (mb_strpos($key, '|') !== false) {
+ [$key, $params] = explode('|', $key, 2);
+ [$template, $language] = mb_strpos($params, '.') !== false ? explode('.', $params, 2) : [$params, ''];
+ } else {
+ $params = $template = $language = '';
+ }
+ $objectKey = basename($key);
+ if (preg_match('|^(\d+)\.(.+)$|', $objectKey, $matches)) {
+ [, $order, $folder] = $matches;
+ } else {
+ [$order, $folder] = ['', $objectKey];
+ }
+
+ $filesystem = Filesystem::getInstance(false);
+
+ $parentKey = ltrim($filesystem->dirname('/' . $key), '/');
+
+ return [
+ 'key' => $key,
+ 'params' => $params,
+ 'parent_key' => $parentKey,
+ 'order' => is_numeric($order) ? (int)$order : null,
+ 'folder' => $folder,
+ 'template' => $template,
+ 'lang' => $language
+ ];
+ }
+
+ /**
+ * @param string $key
+ * @param string $params
+ * @return array
+ */
+ protected function parseParams(string $key, string $params): array
+ {
+ if (mb_strpos($params, '.') !== false) {
+ [$template, $language] = explode('.', $params, 2);
+ } else {
+ $template = $params;
+ $language = '';
+ }
+
+ if ($template === '') {
+ $meta = $this->getObjectMeta($key);
+ $template = $meta['template'] ?? 'folder';
+ }
+
+ return [
+ 'file' => $template . ($language ? '.' . $language : ''),
+ 'template' => $template,
+ 'lang' => $language
+ ];
+ }
+
+ /**
+ * Prepares the row for saving and returns the storage key for the record.
+ *
+ * @param array $row
+ */
+ protected function prepareRow(array &$row): void
+ {
+ // Remove keys used in the filesystem.
+ unset($row['parent_key'], $row['order'], $row['folder'], $row['template'], $row['lang']);
+ }
+
+ /**
+ * @param string $key
+ * @return array
+ */
+ protected function loadRow(string $key): ?array
+ {
+ $data = parent::loadRow($key);
+
+ // Special case for root page.
+ if ($key === '' && null !== $data) {
+ $data['root'] = true;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Page storage supports moving and copying the pages and their languages.
+ *
+ * $row['__META']['copy'] = true Use this if you want to copy the whole folder, otherwise it will be moved
+ * $row['__META']['clone'] = true Use this if you want to clone the file, otherwise it will be renamed
+ *
+ * @param string $key
+ * @param array $row
+ * @return array
+ */
+ protected function saveRow(string $key, array $row): array
+ {
+ // Initialize all key-related variables.
+ $newKeys = $this->extractKeysFromRow($row);
+ $newKey = $this->buildStorageKey($newKeys);
+ $newFolder = $this->buildFolder($newKeys);
+ $newFilename = $this->buildFilename($newKeys);
+ $newFilepath = rtrim($newFolder, '/') !== $newFolder ? $newFolder . $newFilename : $newFolder . '/' . $newFilename;
+
+ try {
+ if ($key === '' && empty($row['root'])) {
+ throw new RuntimeException('Page has no path');
+ }
+
+ $grav = Grav::instance();
+
+ /** @var Debugger $debugger */
+ $debugger = $grav['debugger'];
+ $debugger->addMessage("Save page: {$newKey}", 'debug');
+
+ // Check if the row already exists.
+ $oldKey = $row['__META']['storage_key'] ?? null;
+ if (is_string($oldKey)) {
+ // Initialize all old key-related variables.
+ $oldKeys = $this->extractKeysFromRow(['__META' => $row['__META']], false);
+ $oldFolder = $this->buildFolder($oldKeys);
+ $oldFilename = $this->buildFilename($oldKeys);
+
+ // Check if folder has changed.
+ if ($oldFolder !== $newFolder && file_exists($oldFolder)) {
+ $isCopy = $row['__META']['copy'] ?? false;
+ if ($isCopy) {
+ if (strpos($newFolder, $oldFolder . '/') === 0) {
+ throw new RuntimeException(sprintf('Page /%s cannot be copied to itself', $oldKey));
+ }
+
+ $this->copyRow($oldKey, $newKey);
+ $debugger->addMessage("Page copied: {$oldFolder} => {$newFolder}", 'debug');
+ } else {
+ if (strpos($newFolder, $oldFolder . '/') === 0) {
+ throw new RuntimeException(sprintf('Page /%s cannot be moved to itself', $oldKey));
+ }
+
+ $this->renameRow($oldKey, $newKey);
+ $debugger->addMessage("Page moved: {$oldFolder} => {$newFolder}", 'debug');
+ }
+ }
+
+ // Check if filename has changed.
+ if ($oldFilename !== $newFilename) {
+ // Get instance of the old file (we have already copied/moved it).
+ $oldFilepath = "{$newFolder}/{$oldFilename}";
+ $file = $this->getFile($oldFilepath);
+
+ // Rename the file if we aren't supposed to clone it.
+ $isClone = $row['__META']['clone'] ?? false;
+ if (!$isClone && $file->exists()) {
+ /** @var UniformResourceLocator $locator */
+ $locator = $grav['locator'];
+ $toPath = $locator->isStream($newFilepath) ? $locator->findResource($newFilepath, true, true) : GRAV_ROOT . "/{$newFilepath}";
+ $success = $file->rename($toPath);
+ if (!$success) {
+ throw new RuntimeException("Changing page template failed: {$oldFilepath} => {$newFilepath}");
+ }
+ $debugger->addMessage("Page template changed: {$oldFilename} => {$newFilename}", 'debug');
+ } else {
+ $file = null;
+ $debugger->addMessage("Page template created: {$newFilename}", 'debug');
+ }
+ }
+ }
+
+ // Clean up the data to be saved.
+ $this->prepareRow($row);
+ unset($row['__META'], $row['__ERROR']);
+
+ if (!isset($file)) {
+ $file = $this->getFile($newFilepath);
+ }
+
+ // Compare existing file content to the new one and save the file only if content has been changed.
+ $file->free();
+ $oldRaw = $file->raw();
+ $file->content($row);
+ $newRaw = $file->raw();
+ if ($oldRaw !== $newRaw) {
+ $file->save($row);
+ $debugger->addMessage("Page content saved: {$newFilepath}", 'debug');
+ } else {
+ $debugger->addMessage('Page content has not been changed, do not update the file', 'debug');
+ }
+ } catch (RuntimeException $e) {
+ $name = isset($file) ? $file->filename() : $newKey;
+
+ throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $name, $e->getMessage()));
+ } finally {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $locator->clearCache();
+
+ if (isset($file)) {
+ $file->free();
+ unset($file);
+ }
+ }
+
+ $row['__META'] = $this->getObjectMeta($newKey, true);
+
+ return $row;
+ }
+
+ /**
+ * Check if page folder should be deleted.
+ *
+ * Deleting page can be done either by deleting everything or just a single language.
+ * If key contains the language, delete only it, unless it is the last language.
+ *
+ * @param string $key
+ * @return bool
+ */
+ protected function canDeleteFolder(string $key): bool
+ {
+ // Return true if there's no language in the key.
+ $keys = $this->extractKeysFromStorageKey($key);
+ if (!$keys['lang']) {
+ return true;
+ }
+
+ // Get the main key and reload meta.
+ $key = $this->buildStorageKey($keys);
+ $meta = $this->getObjectMeta($key, true);
+
+ // Return true if there aren't any markdown files left.
+ return empty($meta['markdown'] ?? []);
+ }
+
+ /**
+ * Get key from the filesystem path.
+ *
+ * @param string $path
+ * @return string
+ */
+ protected function getKeyFromPath(string $path): string
+ {
+ if ($this->base_path) {
+ $path = $this->base_path . '/' . $path;
+ }
+
+ return $path;
+ }
+
+ /**
+ * Returns list of all stored keys in [key => timestamp] pairs.
+ *
+ * @return array
+ */
+ protected function buildIndex(): array
+ {
+ $this->clearCache();
+
+ return $this->getIndexMeta();
+ }
+
+ /**
+ * @param string $key
+ * @param bool $reload
+ * @return array
+ */
+ protected function getObjectMeta(string $key, bool $reload = false): array
+ {
+ $keys = $this->extractKeysFromStorageKey($key);
+ $key = $keys['key'];
+
+ if ($reload || !isset($this->meta[$key])) {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ if (mb_strpos($key, '@@') === false) {
+ $path = $this->getStoragePath($key);
+ if (is_string($path)) {
+ $path = $locator->isStream($path) ? $locator->findResource($path) : GRAV_ROOT . "/{$path}";
+ } else {
+ $path = null;
+ }
+ } else {
+ $path = null;
+ }
+
+ $modified = 0;
+ $markdown = [];
+ $children = [];
+
+ if (is_string($path) && is_dir($path)) {
+ $modified = filemtime($path);
+ $iterator = new FilesystemIterator($path, $this->flags);
+
+ /** @var SplFileInfo $info */
+ foreach ($iterator as $k => $info) {
+ // Ignore all hidden files if set.
+ if ($k === '' || ($this->ignore_hidden && $k[0] === '.')) {
+ continue;
+ }
+
+ if ($info->isDir()) {
+ // Ignore all folders in ignore list.
+ if ($this->ignore_folders && in_array($k, $this->ignore_folders, true)) {
+ continue;
+ }
+
+ $children[$k] = false;
+ } else {
+ // Ignore all files in ignore list.
+ if ($this->ignore_files && in_array($k, $this->ignore_files, true)) {
+ continue;
+ }
+
+ $timestamp = $info->getMTime();
+
+ // Page is the one that matches to $page_extensions list with the lowest index number.
+ if (preg_match($this->regex, $k, $matches)) {
+ $mark = $matches[2] ?? '';
+ $ext = $matches[1] ?? '';
+ $ext .= $this->dataExt;
+ $markdown[$mark][basename($k, $ext)] = $timestamp;
+ }
+
+ $modified = max($modified, $timestamp);
+ }
+ }
+ }
+
+ $rawRoute = trim(preg_replace(PageIndex::PAGE_ROUTE_REGEX, '/', "/{$key}") ?? '', '/');
+ $route = PageIndex::normalizeRoute($rawRoute);
+
+ ksort($markdown, SORT_NATURAL | SORT_FLAG_CASE);
+ ksort($children, SORT_NATURAL | SORT_FLAG_CASE);
+
+ $file = array_key_first($markdown[''] ?? (reset($markdown) ?: []));
+
+ $meta = [
+ 'key' => $route,
+ 'storage_key' => $key,
+ 'template' => $file,
+ 'storage_timestamp' => $modified,
+ ];
+ if ($markdown) {
+ $meta['markdown'] = $markdown;
+ }
+ if ($children) {
+ $meta['children'] = $children;
+ }
+ $meta['checksum'] = md5(json_encode($meta) ?: '');
+
+ // Cache meta as copy.
+ $this->meta[$key] = $meta;
+ } else {
+ $meta = $this->meta[$key];
+ }
+
+ $params = $keys['params'];
+ if ($params) {
+ $language = $keys['lang'];
+ $template = $keys['template'] ?: array_key_first($meta['markdown'][$language]) ?? $meta['template'];
+ $meta['exists'] = ($template && !empty($meta['children'])) || isset($meta['markdown'][$language][$template]);
+ $meta['storage_key'] .= '|' . $params;
+ $meta['template'] = $template;
+ $meta['lang'] = $language;
+ }
+
+ return $meta;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getIndexMeta(): array
+ {
+ $queue = [''];
+ $list = [];
+ do {
+ $current = array_pop($queue);
+ if ($current === null) {
+ break;
+ }
+
+ $meta = $this->getObjectMeta($current);
+ $storage_key = $meta['storage_key'];
+
+ if (!empty($meta['children'])) {
+ $prefix = $storage_key . ($storage_key !== '' ? '/' : '');
+
+ foreach ($meta['children'] as $child => $value) {
+ $queue[] = $prefix . $child;
+ }
+ }
+
+ $list[$storage_key] = $meta;
+ } while ($queue);
+
+ ksort($list, SORT_NATURAL | SORT_FLAG_CASE);
+
+ // Update parent timestamps.
+ foreach (array_reverse($list) as $storage_key => $meta) {
+ if ($storage_key !== '') {
+ $filesystem = Filesystem::getInstance(false);
+
+ $storage_key = (string)$storage_key;
+ $parentKey = $filesystem->dirname($storage_key);
+ if ($parentKey === '.') {
+ $parentKey = '';
+ }
+
+ /** @phpstan-var array{'storage_key': string, 'storage_timestamp': int, 'children': array} $parent */
+ $parent = &$list[$parentKey];
+ $basename = basename($storage_key);
+
+ if (isset($parent['children'][$basename])) {
+ $timestamp = $meta['storage_timestamp'];
+ $parent['children'][$basename] = $timestamp;
+ if ($basename && $basename[0] === '_') {
+ $parent['storage_timestamp'] = max($parent['storage_timestamp'], $timestamp);
+ }
+ }
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getNewKey(): string
+ {
+ throw new RuntimeException('Generating random key is disabled for pages');
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php
new file mode 100644
index 00000000..1bde7b00
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php
@@ -0,0 +1,75 @@
+getProperty($property) : null;
+ if (null === $value) {
+ $value = $this->language() . ($var ?? ($this->modified() . md5($this->filePath() ?? $this->getKey())));
+
+ $this->setProperty($property, $value);
+ if ($this->doHasProperty($property)) {
+ $value = $this->getProperty($property);
+ }
+ }
+
+ return $value;
+ }
+
+
+ /**
+ * @inheritdoc
+ */
+ public function date($var = null): int
+ {
+ return $this->loadHeaderProperty(
+ 'date',
+ $var,
+ function ($value) {
+ $value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false;
+
+ if (!$value) {
+ // Get the specific translation updated date.
+ $meta = $this->getMetaData();
+ $language = $meta['lang'] ?? '';
+ $template = $this->getProperty('template');
+ $value = $meta['markdown'][$language][$template] ?? 0;
+ }
+
+ return $value ?: $this->modified();
+ }
+ );
+ }
+
+ /**
+ * @inheritdoc
+ * @param bool $bool
+ */
+ public function isPage(bool $bool = true): bool
+ {
+ $meta = $this->getMetaData();
+
+ return empty($meta['markdown']) !== $bool;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php
new file mode 100644
index 00000000..589ea881
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php
@@ -0,0 +1,236 @@
+path() ?? '';
+
+ return $pages->children($path);
+ }
+
+ /**
+ * Check to see if this item is the first in an array of sub-pages.
+ *
+ * @return bool True if item is first.
+ */
+ public function isFirst(): bool
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::isFirst();
+ }
+
+ $path = $this->path();
+ $parent = $this->parent();
+ $collection = $parent ? $parent->collection('content', false) : null;
+ if (null !== $path && $collection instanceof PageCollectionInterface) {
+ return $collection->isFirst($path);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check to see if this item is the last in an array of sub-pages.
+ *
+ * @return bool True if item is last
+ */
+ public function isLast(): bool
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::isLast();
+ }
+
+ $path = $this->path();
+ $parent = $this->parent();
+ $collection = $parent ? $parent->collection('content', false) : null;
+ if (null !== $path && $collection instanceof PageCollectionInterface) {
+ return $collection->isLast($path);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the adjacent sibling based on a direction.
+ *
+ * @param int $direction either -1 or +1
+ * @return PageInterface|false the sibling page
+ */
+ public function adjacentSibling($direction = 1)
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::adjacentSibling($direction);
+ }
+
+ $path = $this->path();
+ $parent = $this->parent();
+ $collection = $parent ? $parent->collection('content', false) : null;
+ if (null !== $path && $collection instanceof PageCollectionInterface) {
+ $child = $collection->adjacentSibling($path, $direction);
+ if ($child instanceof PageInterface) {
+ return $child;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to return an ancestor page.
+ *
+ * @param string|null $lookup Name of the parent folder
+ * @return PageInterface|null page you were looking for if it exists
+ */
+ public function ancestor($lookup = null)
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::ancestor($lookup);
+ }
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ return $pages->ancestor($this->getProperty('parent_route'), $lookup);
+ }
+
+ /**
+ * Method that contains shared logic for inherited() and inheritedField()
+ *
+ * @param string $field Name of the parent folder
+ * @return array
+ */
+ protected function getInheritedParams($field): array
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::getInheritedParams($field);
+ }
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ $inherited = $pages->inherited($this->getProperty('parent_route'), $field);
+ $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : [];
+ $currentParams = (array)$this->getFormValue('header.' . $field);
+ if ($inheritedParams && is_array($inheritedParams)) {
+ $currentParams = array_replace_recursive($inheritedParams, $currentParams);
+ }
+
+ return [$inherited, $currentParams];
+ }
+
+ /**
+ * Helper method to return a page.
+ *
+ * @param string $url the url of the page
+ * @param bool $all
+ * @return PageInterface|null page you were looking for if it exists
+ */
+ public function find($url, $all = false)
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::find($url, $all);
+ }
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ return $pages->find($url, $all);
+ }
+
+ /**
+ * Get a collection of pages in the current context.
+ *
+ * @param string|array $params
+ * @param bool $pagination
+ * @return PageCollectionInterface|Collection
+ * @throws InvalidArgumentException
+ */
+ public function collection($params = 'content', $pagination = true)
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::collection($params, $pagination);
+ }
+
+ if (is_string($params)) {
+ // Look into a page header field.
+ $params = (array)$this->getFormValue('header.' . $params);
+ } elseif (!is_array($params)) {
+ throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
+ }
+
+ $context = [
+ 'pagination' => $pagination,
+ 'self' => $this
+ ];
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ return $pages->getCollection($params, $context);
+ }
+
+ /**
+ * @param string|array $value
+ * @param bool $only_published
+ * @return PageCollectionInterface|Collection
+ */
+ public function evaluate($value, $only_published = true)
+ {
+ if (Utils::isAdminPlugin()) {
+ return parent::collection($value, $only_published);
+ }
+
+ $params = [
+ 'items' => $value,
+ 'published' => $only_published
+ ];
+ $context = [
+ 'event' => false,
+ 'pagination' => false,
+ 'url_taxonomy_filters' => false,
+ 'self' => $this
+ ];
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ return $pages->getCollection($params, $context);
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
new file mode 100644
index 00000000..3b490a59
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php
@@ -0,0 +1,122 @@
+root()) {
+ return null;
+ }
+
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+
+ $filesystem = Filesystem::getInstance(false);
+
+ // FIXME: this does not work, needs to use $pages->get() with cached parent id!
+ $key = $this->getKey();
+ $parent_route = $filesystem->dirname('/' . $key);
+
+ return $parent_route !== '/' ? $pages->find($parent_route) : $pages->root();
+ }
+
+ /**
+ * Returns the item in the current position.
+ *
+ * @return int|null the index of the current page.
+ */
+ public function currentPosition(): ?int
+ {
+ $path = $this->path();
+ $parent = $this->parent();
+ $collection = $parent ? $parent->collection('content', false) : null;
+ if (null !== $path && $collection instanceof PageCollectionInterface) {
+ return $collection->currentPosition($path);
+ }
+
+ return 1;
+ }
+
+ /**
+ * Returns whether or not this page is the currently active page requested via the URL.
+ *
+ * @return bool True if it is active
+ */
+ public function active(): bool
+ {
+ $grav = Grav::instance();
+ $uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/';
+ $routes = $grav['pages']->routes();
+
+ return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path();
+ }
+
+ /**
+ * Returns whether or not this URI's URL contains the URL of the active page.
+ * Or in other words, is this page's URL in the current URL
+ *
+ * @return bool True if active child exists
+ */
+ public function activeChild(): bool
+ {
+ $grav = Grav::instance();
+ /** @var Uri $uri */
+ $uri = $grav['uri'];
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
+ $uri_path = rtrim(urldecode($uri->path()), '/');
+ $routes = $pages->routes();
+
+ if (isset($routes[$uri_path])) {
+ $page = $pages->find($uri->route());
+ /** @var PageInterface|null $child_page */
+ $child_page = $page ? $page->parent() : null;
+ while ($child_page && !$child_page->root()) {
+ if ($this->path() === $child_page->path()) {
+ return true;
+ }
+ $child_page = $child_page->parent();
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
new file mode 100644
index 00000000..dff42c95
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php
@@ -0,0 +1,108 @@
+getLanguageTemplates();
+ if (!$translated) {
+ return $translated;
+ }
+
+ $grav = Grav::instance();
+
+ /** @var Language $language */
+ $language = $grav['language'];
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $grav['locator'];
+
+ $languages = $language->getLanguages();
+ $languages[] = '';
+ $defaultCode = $language->getDefault();
+
+ if (isset($translated[$defaultCode])) {
+ unset($translated['']);
+ }
+
+ foreach ($translated as $key => &$template) {
+ $template .= $key !== '' ? ".{$key}.md" : '.md';
+ }
+ unset($template);
+
+ $translated = array_intersect_key($translated, array_flip($languages));
+
+ $folder = $this->getStorageFolder();
+ if (!$folder) {
+ return [];
+ }
+ $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
+
+ $list = array_fill_keys($languages, null);
+ foreach ($translated as $languageCode => $languageFile) {
+ $languageExtension = $languageCode ? ".{$languageCode}.md" : '.md';
+ $path = "{$folder}/{$languageFile}";
+
+ // FIXME: use flex, also rawRoute() does not fully work?
+ $aPage = new Page();
+ $aPage->init(new SplFileInfo($path), $languageExtension);
+ if ($onlyPublished && !$aPage->published()) {
+ continue;
+ }
+
+ $header = $aPage->header();
+ // @phpstan-ignore-next-line
+ $routes = $header->routes ?? [];
+ $route = $routes['default'] ?? $aPage->rawRoute();
+ if (!$route) {
+ $route = $aPage->route();
+ }
+
+ $list[$languageCode ?: $defaultCode] = $route ?? '';
+ }
+
+ $list = array_filter($list, static function ($var) {
+ return null !== $var;
+ });
+
+ // Hack to get the same result as with old pages.
+ foreach ($list as &$path) {
+ if ($path === '') {
+ $path = null;
+ }
+ }
+
+ return $list;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php
new file mode 100644
index 00000000..93abbf88
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php
@@ -0,0 +1,56 @@
+
+ */
+class UserGroupCollection extends FlexCollection
+{
+ /**
+ * @return array
+ */
+ public static function getCachedMethods(): array
+ {
+ return [
+ 'authorize' => 'session',
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * Checks user authorization to the action.
+ *
+ * @param string $action
+ * @param string|null $scope
+ * @return bool|null
+ */
+ public function authorize(string $action, string $scope = null): ?bool
+ {
+ $authorized = null;
+ /** @var UserGroupObject $object */
+ foreach ($this as $object) {
+ $auth = $object->authorize($action, $scope);
+ if ($auth === true) {
+ $authorized = true;
+ } elseif ($auth === false) {
+ return false;
+ }
+ }
+
+ return $authorized;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php
new file mode 100644
index 00000000..4bee5ac3
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php
@@ -0,0 +1,24 @@
+
+ */
+class UserGroupIndex extends FlexIndex
+{
+}
diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
new file mode 100644
index 00000000..ea68fa1b
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
@@ -0,0 +1,121 @@
+ 'session',
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return $this->getProperty('readableName');
+ }
+
+ /**
+ * Checks user authorization to the action.
+ *
+ * @param string $action
+ * @param string|null $scope
+ * @return bool|null
+ */
+ public function authorize(string $action, string $scope = null): ?bool
+ {
+ if ($scope === 'test') {
+ $scope = null;
+ } elseif (!$this->getProperty('enabled', true)) {
+ return null;
+ }
+
+ $access = $this->getAccess();
+
+ $authorized = $access->authorize($action, $scope);
+ if (is_bool($authorized)) {
+ return $authorized;
+ }
+
+ return $access->authorize('admin.super') ? true : null;
+ }
+
+ /**
+ * @return Access
+ */
+ protected function getAccess(): Access
+ {
+ if (null === $this->_access) {
+ $this->getProperty('access');
+ }
+
+ return $this->_access;
+ }
+
+ /**
+ * @param mixed $value
+ * @return array
+ */
+ protected function offsetLoad_access($value): array
+ {
+ if (!$value instanceof Access) {
+ $value = new Access($value);
+ }
+
+ $this->_access = $value;
+
+ return $value->jsonSerialize();
+ }
+
+ /**
+ * @param mixed $value
+ * @return array
+ */
+ protected function offsetPrepare_access($value): array
+ {
+ return $this->offsetLoad_access($value);
+ }
+
+ /**
+ * @param array|null $value
+ * @return array|null
+ */
+ protected function offsetSerialize_access(?array $value): ?array
+ {
+ return $value;
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php
new file mode 100644
index 00000000..f565c9f1
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php
@@ -0,0 +1,47 @@
+update($data)` instead (same but with data validation & filtering, file upload support).
+ */
+ public function merge(array $data)
+ {
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED);
+
+ $this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data));
+
+ return $this;
+ }
+
+ /**
+ * Return media object for the User's avatar.
+ *
+ * @return ImageMedium|StaticImageMedium|null
+ * @deprecated 1.6 Use ->getAvatarImage() method instead.
+ */
+ public function getAvatarMedia()
+ {
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED);
+
+ return $this->getAvatarImage();
+ }
+
+ /**
+ * Return the User's avatar URL
+ *
+ * @return string
+ * @deprecated 1.6 Use ->getAvatarUrl() method instead.
+ */
+ public function avatarUrl()
+ {
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED);
+
+ return $this->getAvatarUrl();
+ }
+
+ /**
+ * Checks user authorization to the action.
+ * Ensures backwards compatibility
+ *
+ * @param string $action
+ * @return bool
+ * @deprecated 1.5 Use ->authorize() method instead.
+ */
+ public function authorise($action)
+ {
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED);
+
+ return $this->authorize($action) ?? false;
+ }
+
+ /**
+ * Implements Countable interface.
+ *
+ * @return int
+ * @deprecated 1.6 Method makes no sense for user account.
+ */
+ public function count()
+ {
+ user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
+
+ return count($this->jsonSerialize());
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Users/UserCollection.php b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php
new file mode 100644
index 00000000..9b6490a8
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php
@@ -0,0 +1,135 @@
+
+ */
+class UserCollection extends FlexCollection implements UserCollectionInterface
+{
+ /**
+ * @return array
+ */
+ public static function getCachedMethods(): array
+ {
+ return [
+ 'authorize' => 'session',
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * Load user account.
+ *
+ * Always creates user object. To check if user exists, use $this->exists().
+ *
+ * @param string $username
+ * @return UserObject
+ */
+ public function load($username): UserInterface
+ {
+ $username = (string)$username;
+
+ if ($username !== '') {
+ $key = $this->filterUsername($username);
+ $user = $this->get($key);
+ if ($user) {
+ return $user;
+ }
+ } else {
+ $key = '';
+ }
+
+ $directory = $this->getFlexDirectory();
+
+ /** @var UserObject $object */
+ $object = $directory->createObject(
+ [
+ 'username' => $username,
+ 'state' => 'enabled'
+ ],
+ $key
+ );
+
+ return $object;
+ }
+
+ /**
+ * Find a user by username, email, etc
+ *
+ * @param string $query the query to search for
+ * @param string|string[] $fields the fields to search
+ * @return UserObject
+ */
+ public function find($query, $fields = ['username', 'email']): UserInterface
+ {
+ if (is_string($query) && $query !== '') {
+ foreach ((array)$fields as $field) {
+ if ($field === 'key') {
+ $user = $this->get($query);
+ } elseif ($field === 'storage_key') {
+ $user = $this->withKeyField('storage_key')->get($query);
+ } elseif ($field === 'flex_key') {
+ $user = $this->withKeyField('flex_key')->get($query);
+ } elseif ($field === 'username') {
+ $user = $this->get($this->filterUsername($query));
+ } else {
+ $user = parent::find($query, $field);
+ }
+ if ($user instanceof UserObject) {
+ return $user;
+ }
+ }
+ }
+
+ return $this->load('');
+ }
+
+ /**
+ * Delete user account.
+ *
+ * @param string $username
+ * @return bool True if user account was found and was deleted.
+ */
+ public function delete($username): bool
+ {
+ $user = $this->load($username);
+
+ $exists = $user->exists();
+ if ($exists) {
+ $user->delete();
+ }
+
+ return $exists;
+ }
+
+ /**
+ * @param string $key
+ * @return string
+ */
+ protected function filterUsername(string $key): string
+ {
+ $storage = $this->getFlexDirectory()->getStorage();
+ if (method_exists($storage, 'normalizeKey')) {
+ return $storage->normalizeKey($key);
+ }
+
+ return mb_strtolower($key);
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php
new file mode 100644
index 00000000..6e0bc65f
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php
@@ -0,0 +1,204 @@
+
+ */
+class UserIndex extends FlexIndex implements UserCollectionInterface
+{
+ public const VERSION = parent::VERSION . '.1';
+
+ /**
+ * @param FlexStorageInterface $storage
+ * @return array
+ */
+ public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
+ {
+ // Load saved index.
+ $index = static::loadIndex($storage);
+
+ $version = $index['version'] ?? 0;
+ $force = static::VERSION !== $version;
+
+ // TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found.
+ //$timestamp = $index['timestamp'] ?? 0;
+ //if (!$force && $timestamp && $timestamp > time() - 1) {
+ // return $index['index'];
+ //}
+
+ // Load up to date index.
+ $entries = parent::loadEntriesFromStorage($storage);
+
+ return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]);
+ }
+
+ /**
+ * @param array $meta
+ * @param array $data
+ * @param FlexStorageInterface $storage
+ * @return void
+ */
+ public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage): void
+ {
+ // Username can also be number and stored as such.
+ $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
+ $meta['key'] = static::filterUsername($key, $storage);
+ $meta['email'] = isset($data['email']) ? mb_strtolower($data['email']) : null;
+ }
+
+ /**
+ * Load user account.
+ *
+ * Always creates user object. To check if user exists, use $this->exists().
+ *
+ * @param string $username
+ * @return UserObject
+ */
+ public function load($username): UserInterface
+ {
+ $username = (string)$username;
+
+ if ($username !== '') {
+ $key = static::filterUsername($username, $this->getFlexDirectory()->getStorage());
+ $user = $this->get($key);
+ if ($user) {
+ return $user;
+ }
+ } else {
+ $key = '';
+ }
+
+ $directory = $this->getFlexDirectory();
+
+ /** @var UserObject $object */
+ $object = $directory->createObject(
+ [
+ 'username' => $username,
+ 'state' => 'enabled'
+ ],
+ $key
+ );
+
+ return $object;
+ }
+
+ /**
+ * Delete user account.
+ *
+ * @param string $username
+ * @return bool True if user account was found and was deleted.
+ */
+ public function delete($username): bool
+ {
+ $user = $this->load($username);
+
+ $exists = $user->exists();
+ if ($exists) {
+ $user->delete();
+ }
+
+ return $exists;
+ }
+
+ /**
+ * Find a user by username, email, etc
+ *
+ * @param string $query the query to search for
+ * @param array $fields the fields to search
+ * @return UserObject
+ */
+ public function find($query, $fields = ['username', 'email']): UserInterface
+ {
+ if (is_string($query) && $query !== '') {
+ foreach ((array)$fields as $field) {
+ if ($field === 'key') {
+ $user = $this->get($query);
+ } elseif ($field === 'storage_key') {
+ $user = $this->withKeyField('storage_key')->get($query);
+ } elseif ($field === 'flex_key') {
+ $user = $this->withKeyField('flex_key')->get($query);
+ } elseif ($field === 'email') {
+ $user = $this->withKeyField('email')->get($query);
+ } elseif ($field === 'username') {
+ $user = $this->get(static::filterUsername($query, $this->getFlexDirectory()->getStorage()));
+ } else {
+ $user = $this->__call('find', [$query, $field]);
+ }
+ if ($user) {
+ return $user;
+ }
+ }
+ }
+
+ return $this->load('');
+ }
+
+ /**
+ * @param string $key
+ * @param FlexStorageInterface $storage
+ * @return string
+ */
+ protected static function filterUsername(string $key, FlexStorageInterface $storage): string
+ {
+ return $storage->normalizeKey($key);
+ }
+
+ /**
+ * @param FlexStorageInterface $storage
+ * @return CompiledYamlFile|null
+ */
+ protected static function getIndexFile(FlexStorageInterface $storage)
+ {
+ // Load saved index file.
+ $grav = Grav::instance();
+ $locator = $grav['locator'];
+ $filename = $locator->findResource('user-data://flex/indexes/accounts.yaml', true, true);
+
+ return CompiledYamlFile::instance($filename);
+ }
+
+ /**
+ * @param array $entries
+ * @param array $added
+ * @param array $updated
+ * @param array $removed
+ */
+ protected static function onChanges(array $entries, array $added, array $updated, array $removed): void
+ {
+ $message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed));
+
+ $grav = Grav::instance();
+
+ /** @var Logger $logger */
+ $logger = $grav['log'];
+ $logger->addDebug($message);
+
+ /** @var Debugger $debugger */
+ $debugger = $grav['debugger'];
+ $debugger->addMessage($message, 'debug');
+ }
+}
diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php
new file mode 100644
index 00000000..310c313f
--- /dev/null
+++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php
@@ -0,0 +1,939 @@
+ 'session',
+ 'load' => false,
+ 'find' => false,
+ 'remove' => false,
+ 'get' => true,
+ 'set' => false,
+ 'undef' => false,
+ 'def' => false,
+ ] + parent::getCachedMethods();
+ }
+
+ /**
+ * UserObject constructor.
+ * @param array $elements
+ * @param string $key
+ * @param FlexDirectory $directory
+ * @param bool $validate
+ */
+ public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
+ {
+ // User can only be authenticated via login.
+ unset($elements['authenticated'], $elements['authorized']);
+
+ // Define username if it's not set.
+ if (!isset($elements['username'])) {
+ $storageKey = $elements['__META']['storage_key'] ?? null;
+ if (null !== $storageKey && $key === $directory->getStorage()->normalizeKey($storageKey)) {
+ $elements['username'] = $storageKey;
+ } else {
+ $elements['username'] = $key;
+ }
+ }
+
+ // Define state if it isn't set.
+ if (!isset($elements['state'])) {
+ $elements['state'] = 'enabled';
+ }
+
+ parent::__construct($elements, $key, $directory, $validate);
+ }
+
+ /**
+ * @return void
+ */
+ public function onPrepareRegistration(): void
+ {
+ if (!$this->getProperty('access')) {
+ /** @var Config $config */
+ $config = Grav::instance()['config'];
+
+ $groups = $config->get('plugins.login.user_registration.groups', '');
+ $access = $config->get('plugins.login.user_registration.access', ['site' => ['login' => true]]);
+
+ $this->setProperty('groups', $groups);
+ $this->setProperty('access', $access);
+ }
+ }
+
+ /**
+ * Helper to get content editor will fall back if not set
+ *
+ * @return string
+ */
+ public function getContentEditor(): string
+ {
+ return $this->getProperty('content_editor', 'default');
+ }
+
+ /**
+ * Get value by using dot notation for nested arrays/objects.
+ *
+ * @example $value = $this->get('this.is.my.nested.variable');
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $default Default value (or null).
+ * @param string|null $separator Separator, defaults to '.'
+ * @return mixed Value.
+ */
+ public function get($name, $default = null, $separator = null)
+ {
+ return $this->getNestedProperty($name, $default, $separator);
+ }
+
+ /**
+ * Set value by using dot notation for nested arrays/objects.
+ *
+ * @example $data->set('this.is.my.nested.variable', $value);
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $value New value.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ public function set($name, $value, $separator = null)
+ {
+ $this->setNestedProperty($name, $value, $separator);
+
+ return $this;
+ }
+
+ /**
+ * Unset value by using dot notation for nested arrays/objects.
+ *
+ * @example $data->undef('this.is.my.nested.variable');
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ public function undef($name, $separator = null)
+ {
+ $this->unsetNestedProperty($name, $separator);
+
+ return $this;
+ }
+
+ /**
+ * Set default value by using dot notation for nested arrays/objects.
+ *
+ * @example $data->def('this.is.my.nested.variable', 'default');
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $default Default value (or null).
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ public function def($name, $default = null, $separator = null)
+ {
+ $this->defNestedProperty($name, $default, $separator);
+
+ return $this;
+ }
+
+ /**
+ * Checks user authorization to the action.
+ *
+ * @param string $action
+ * @param string|null $scope
+ * @return bool|null
+ */
+ public function authorize(string $action, string $scope = null): ?bool
+ {
+ if ($scope === 'test') {
+ // Special scope to test user permissions.
+ $scope = null;
+ } else {
+ // User needs to be enabled.
+ if ($this->getProperty('state') !== 'enabled') {
+ return false;
+ }
+
+ // User needs to be logged in.
+ if (!$this->getProperty('authenticated')) {
+ return false;
+ }
+
+ if (strpos($action, 'login') === false && !$this->getProperty('authorized')) {
+ // User needs to be authorized (2FA).
+ return false;
+ }
+
+ // Workaround bug in Login::isUserAuthorizedForPage() <= Login v3.0.4
+ if ((string)(int)$action === $action) {
+ return false;
+ }
+ }
+
+ $authorizeCallable = static::$authorizeCallable;
+ if ($authorizeCallable instanceof Closure) {
+ $authorizeCallable->bindTo($this);
+ $authorized = $authorizeCallable($action, $scope);
+ if (is_bool($authorized)) {
+ return $authorized;
+ }
+ }
+
+ // Check user access.
+ $access = $this->getAccess();
+ $authorized = $access->authorize($action, $scope);
+ if (is_bool($authorized)) {
+ return $authorized;
+ }
+
+ // If specific rule isn't hit, check if user is super user.
+ if ($access->authorize('admin.super') === true) {
+ return true;
+ }
+
+ // Check group access.
+ return $this->getGroups()->authorize($action, $scope);
+ }
+
+ /**
+ * @param string $property
+ * @param mixed $default
+ * @return mixed
+ */
+ public function getProperty($property, $default = null)
+ {
+ $value = parent::getProperty($property, $default);
+
+ if ($property === 'avatar') {
+ $settings = $this->getMediaFieldSettings($property);
+ $value = $this->parseFileProperty($value, $settings);
+ }
+
+ return $value;
+ }
+
+ /**
+ * @return UserGroupIndex
+ */
+ public function getRoles(): UserGroupIndex
+ {
+ return $this->getGroups();
+ }
+
+ /**
+ * Convert object into an array.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $array = $this->jsonSerialize();
+
+ $settings = $this->getMediaFieldSettings('avatar');
+ $array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null, $settings);
+
+ return $array;
+ }
+
+ /**
+ * Convert object into YAML string.
+ *
+ * @param int $inline The level where you switch to inline YAML.
+ * @param int $indent The amount of spaces to use for indentation of nested nodes.
+ * @return string A YAML string representing the object.
+ */
+ public function toYaml($inline = 5, $indent = 2)
+ {
+ $yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]);
+
+ return $yaml->encode($this->toArray());
+ }
+
+ /**
+ * Convert object into JSON string.
+ *
+ * @return string
+ */
+ public function toJson()
+ {
+ $json = new JsonFormatter();
+
+ return $json->encode($this->toArray());
+ }
+
+ /**
+ * Join nested values together by using blueprints.
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $value Value to be joined.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ * @throws RuntimeException
+ */
+ public function join($name, $value, $separator = null)
+ {
+ $separator = $separator ?? '.';
+ $old = $this->get($name, null, $separator);
+ if ($old !== null) {
+ if (!is_array($old)) {
+ throw new RuntimeException('Value ' . $old);
+ }
+
+ if (is_object($value)) {
+ $value = (array) $value;
+ } elseif (!is_array($value)) {
+ throw new RuntimeException('Value ' . $value);
+ }
+
+ $value = $this->getBlueprint()->mergeData($old, $value, $name, $separator);
+ }
+
+ $this->set($name, $value, $separator);
+
+ return $this;
+ }
+
+ /**
+ * Get nested structure containing default values defined in the blueprints.
+ *
+ * Fields without default value are ignored in the list.
+
+ * @return array
+ */
+ public function getDefaults()
+ {
+ return $this->getBlueprint()->getDefaults();
+ }
+
+ /**
+ * Set default values by using blueprints.
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $value Value to be joined.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ public function joinDefaults($name, $value, $separator = null)
+ {
+ if (is_object($value)) {
+ $value = (array) $value;
+ }
+
+ $old = $this->get($name, null, $separator);
+ if ($old !== null) {
+ $value = $this->getBlueprint()->mergeData($value, $old, $name, $separator ?? '.');
+ }
+
+ $this->setNestedProperty($name, $value, $separator);
+
+ return $this;
+ }
+
+ /**
+ * Get value from the configuration and join it with given data.
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param array|object $value Value to be joined.
+ * @param string $separator Separator, defaults to '.'
+ * @return array
+ * @throws RuntimeException
+ */
+ public function getJoined($name, $value, $separator = null)
+ {
+ if (is_object($value)) {
+ $value = (array) $value;
+ } elseif (!is_array($value)) {
+ throw new RuntimeException('Value ' . $value);
+ }
+
+ $old = $this->get($name, null, $separator);
+
+ if ($old === null) {
+ // No value set; no need to join data.
+ return $value;
+ }
+
+ if (!is_array($old)) {
+ throw new RuntimeException('Value ' . $old);
+ }
+
+ // Return joined data.
+ return $this->getBlueprint()->mergeData($old, $value, $name, $separator ?? '.');
+ }
+
+ /**
+ * Set default values to the configuration if variables were not set.
+ *
+ * @param array $data
+ * @return $this
+ */
+ public function setDefaults(array $data)
+ {
+ $this->setElements($this->getBlueprint()->mergeData($data, $this->toArray()));
+
+ return $this;
+ }
+
+ /**
+ * Validate by blueprints.
+ *
+ * @return $this
+ * @throws \Exception
+ */
+ public function validate()
+ {
+ $this->getBlueprint()->validate($this->toArray());
+
+ return $this;
+ }
+
+ /**
+ * Filter all items by using blueprints.
+ * @return $this
+ */
+ public function filter()
+ {
+ $this->setElements($this->getBlueprint()->filter($this->toArray()));
+
+ return $this;
+ }
+
+ /**
+ * Get extra items which haven't been defined in blueprints.
+ *
+ * @return array
+ */
+ public function extra()
+ {
+ return $this->getBlueprint()->extra($this->toArray());
+ }
+
+ /**
+ * Return unmodified data as raw string.
+ *
+ * NOTE: This function only returns data which has been saved to the storage.
+ *
+ * @return string
+ */
+ public function raw()
+ {
+ $file = $this->file();
+
+ return $file ? $file->raw() : '';
+ }
+
+ /**
+ * Set or get the data storage.
+ *
+ * @param FileInterface|null $storage Optionally enter a new storage.
+ * @return FileInterface|null
+ */
+ public function file(FileInterface $storage = null)
+ {
+ if (null !== $storage) {
+ $this->_storage = $storage;
+ }
+
+ return $this->_storage;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid(): bool
+ {
+ return $this->getProperty('state') !== null;
+ }
+
+ /**
+ * Save user
+ *
+ * @return static
+ */
+ public function save()
+ {
+ // TODO: We may want to handle this in the storage layer in the future.
+ $key = $this->getStorageKey();
+ if (!$key || strpos($key, '@@')) {
+ $storage = $this->getFlexDirectory()->getStorage();
+ if ($storage instanceof FileStorage) {
+ $this->setStorageKey($this->getKey());
+ }
+ }
+
+ $password = $this->getProperty('password') ?? $this->getProperty('password1');
+ if (null !== $password && '' !== $password) {
+ $password2 = $this->getProperty('password2');
+ if (!\is_string($password) || ($password2 && $password !== $password2)) {
+ throw new \RuntimeException('Passwords did not match.');
+ }
+
+ $this->setProperty('hashed_password', Authentication::create($password));
+ }
+ $this->unsetProperty('password');
+ $this->unsetProperty('password1');
+ $this->unsetProperty('password2');
+
+ // Backwards compatibility with older plugins.
+ $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
+ $grav = $this->getContainer();
+ if ($fireEvents) {
+ $self = $this;
+ $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
+ if ($self !== $this) {
+ throw new RuntimeException('Switching Flex User object during onAdminSave event is not supported! Please update plugin.');
+ }
+ }
+
+ $instance = parent::save();
+
+ // Backwards compatibility with older plugins.
+ if ($fireEvents) {
+ $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
+ }
+
+ return $instance;
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareStorage(): array
+ {
+ $elements = parent::prepareStorage();
+
+ // Do not save authorization information.
+ unset($elements['authenticated'], $elements['authorized']);
+
+ return $elements;
+ }
+
+ /**
+ * @return MediaCollectionInterface
+ */
+ public function getMedia()
+ {
+ /** @var Media $media */
+ $media = $this->getFlexMedia();
+
+ // Deal with shared avatar folder.
+ $path = $this->getAvatarFile();
+ if ($path && !$media[$path] && is_file($path)) {
+ $medium = MediumFactory::fromFile($path);
+ if ($medium) {
+ $media->add($path, $medium);
+ $name = basename($path);
+ if ($name !== $path) {
+ $media->add($name, $medium);
+ }
+ }
+ }
+
+ return $media;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getMediaFolder(): ?string
+ {
+ $folder = $this->getFlexMediaFolder();
+
+ // Check for shared media
+ if (!$folder && !$this->getFlexDirectory()->getMediaFolder()) {
+ $this->_loadMedia = false;
+ $folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'user://accounts/avatars';
+ }
+
+ return $folder;
+ }
+
+ /**
+ * @param string $name
+ * @return Blueprint
+ */
+ protected function doGetBlueprint(string $name = ''): Blueprint
+ {
+ $blueprint = $this->getFlexDirectory()->getBlueprint($name ? '.' . $name : $name);
+
+ // HACK: With folder storage we need to ignore the avatar destination.
+ if ($this->getFlexDirectory()->getMediaFolder()) {
+ $field = $blueprint->get('form/fields/avatar');
+ if ($field) {
+ unset($field['destination']);
+ $blueprint->set('form/fields/avatar', $field);
+ }
+ }
+
+ return $blueprint;
+ }
+
+ /**
+ * @param UserInterface $user
+ * @param string $action
+ * @param string $scope
+ * @param bool $isMe
+ * @return bool|null
+ */
+ protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe = false): ?bool
+ {
+ if ($user instanceof self && $user->getStorageKey() === $this->getStorageKey()) {
+ // User cannot delete his own account, otherwise he has full access.
+ return $action !== 'delete';
+ }
+
+ return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
+ }
+
+ /**
+ * @return string|null
+ */
+ protected function getAvatarFile(): ?string
+ {
+ $avatars = $this->getElement('avatar');
+ if (is_array($avatars) && $avatars) {
+ $avatar = array_shift($avatars);
+
+ return $avatar['path'] ?? null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the associated media collection (original images).
+ *
+ * @return MediaCollectionInterface Representation of associated media.
+ */
+ protected function getOriginalMedia()
+ {
+ $folder = $this->getMediaFolder();
+ if ($folder) {
+ $folder .= '/original';
+ }
+
+ return (new Media($folder ?? '', $this->getMediaOrder()))->setTimestamps();
+ }
+
+ /**
+ * @param array $files
+ * @return void
+ */
+ protected function setUpdatedMedia(array $files): void
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+
+ $media = $this->getMedia();
+ if (!$media instanceof MediaUploadInterface) {
+ return;
+ }
+
+ $filesystem = Filesystem::getInstance(false);
+
+ $list = [];
+ $list_original = [];
+ foreach ($files as $field => $group) {
+ // Ignore files without a field.
+ if ($field === '') {
+ continue;
+ }
+ $field = (string)$field;
+
+ // Load settings for the field.
+ $settings = $this->getMediaFieldSettings($field);
+ foreach ($group as $filename => $file) {
+ if ($file) {
+ // File upload.
+ $filename = $file->getClientFilename();
+
+ /** @var FormFlashFile $file */
+ $data = $file->jsonSerialize();
+ unset($data['tmp_name'], $data['path']);
+ } else {
+ // File delete.
+ $data = null;
+ }
+
+ if ($file) {
+ // Check file upload against media limits (except for max size).
+ $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
+ }
+
+ $self = $settings['self'];
+ if ($this->_loadMedia && $self) {
+ $filepath = $filename;
+ } else {
+ $filepath = "{$settings['destination']}/{$filename}";
+
+ // For backwards compatibility we are always using relative path from the installation root.
+ if ($locator->isStream($filepath)) {
+ $filepath = $locator->findResource($filepath, false, true);
+ }
+ }
+
+ // Special handling for original images.
+ if (strpos($field, '/original')) {
+ if ($this->_loadMedia && $self) {
+ $list_original[$filename] = [$file, $settings];
+ }
+ continue;
+ }
+
+ // Calculate path without the retina scaling factor.
+ $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
+
+ $list[$filename] = [$file, $settings];
+
+ $path = str_replace('.', "\n", $field);
+ if (null !== $data) {
+ $data['name'] = $filename;
+ $data['path'] = $filepath;
+
+ $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
+ } else {
+ $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
+ }
+ }
+ }
+
+ $this->clearMediaCache();
+
+ $this->_uploads = $list;
+ $this->_uploads_original = $list_original;
+ }
+
+ protected function saveUpdatedMedia(): void
+ {
+ $media = $this->getMedia();
+ if (!$media instanceof MediaUploadInterface) {
+ throw new RuntimeException('Internal error UO101');
+ }
+
+ // Upload/delete original sized images.
+ /**
+ * @var string $filename
+ * @var UploadedFileInterface|array|null $file
+ */
+ foreach ($this->_uploads_original ?? [] as $filename => $file) {
+ $filename = 'original/' . $filename;
+ if (is_array($file)) {
+ [$file, $settings] = $file;
+ } else {
+ $settings = null;
+ }
+ if ($file instanceof UploadedFileInterface) {
+ $media->copyUploadedFile($file, $filename, $settings);
+ } else {
+ $media->deleteFile($filename, $settings);
+ }
+ }
+
+ // Upload/delete altered files.
+ /**
+ * @var string $filename
+ * @var UploadedFileInterface|array|null $file
+ */
+ foreach ($this->getUpdatedMedia() as $filename => $file) {
+ if (is_array($file)) {
+ [$file, $settings] = $file;
+ } else {
+ $settings = null;
+ }
+ if ($file instanceof UploadedFileInterface) {
+ $media->copyUploadedFile($file, $filename, $settings);
+ } else {
+ $media->deleteFile($filename, $settings);
+ }
+ }
+
+ $this->setUpdatedMedia([]);
+ $this->clearMediaCache();
+ }
+
+ /**
+ * @return array
+ */
+ protected function doSerialize(): array
+ {
+ return [
+ 'type' => $this->getFlexType(),
+ 'key' => $this->getKey(),
+ 'elements' => $this->jsonSerialize(),
+ 'storage' => $this->getMetaData()
+ ];
+ }
+
+ /**
+ * @return UserGroupIndex
+ */
+ protected function getUserGroups()
+ {
+ $grav = Grav::instance();
+
+ /** @var Flex $flex */
+ $flex = $grav['flex'];
+
+ /** @var UserGroupCollection|null $groups */
+ $groups = $flex->getDirectory('user-groups');
+ if ($groups) {
+ /** @var UserGroupIndex $index */
+ $index = $groups->getIndex();
+
+ return $index;
+ }
+
+ return $grav['user_groups'];
+ }
+
+ /**
+ * @return UserGroupIndex
+ */
+ protected function getGroups()
+ {
+ if (null === $this->_groups) {
+ $this->_groups = $this->getUserGroups()->select((array)$this->getProperty('groups'));
+ }
+
+ return $this->_groups;
+ }
+
+ /**
+ * @return Access
+ */
+ protected function getAccess(): Access
+ {
+ if (null === $this->_access) {
+ $this->getProperty('access');
+ }
+
+ return $this->_access;
+ }
+
+ /**
+ * @param mixed $value
+ * @return array
+ */
+ protected function offsetLoad_access($value): array
+ {
+ if (!$value instanceof Access) {
+ $value = new Access($value);
+ }
+
+ $this->_access = $value;
+
+ return $value->jsonSerialize();
+ }
+
+ /**
+ * @param mixed $value
+ * @return array
+ */
+ protected function offsetPrepare_access($value): array
+ {
+ return $this->offsetLoad_access($value);
+ }
+
+ /**
+ * @param array|null $value
+ * @return array|null
+ */
+ protected function offsetSerialize_access(?array $value): ?array
+ {
+ return $value;
+ }
+}
diff --git a/system/src/Grav/Common/Form/FormFlash.php b/system/src/Grav/Common/Form/FormFlash.php
index b67313bd..430f15e0 100644
--- a/system/src/Grav/Common/Form/FormFlash.php
+++ b/system/src/Grav/Common/Form/FormFlash.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Form
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,7 +11,12 @@ namespace Grav\Common\Form;
use Grav\Common\Filesystem\Folder;
use Grav\Framework\Form\FormFlash as FrameworkFormFlash;
+use function is_array;
+/**
+ * Class FormFlash
+ * @package Grav\Common\Form
+ */
class FormFlash extends FrameworkFormFlash
{
/**
@@ -26,7 +31,7 @@ class FormFlash extends FrameworkFormFlash
continue;
}
foreach ($files as $file) {
- if (\is_array($file)) {
+ if (is_array($file)) {
$file['tmp_name'] = $this->getTmpDir() . '/' . $file['tmp_name'];
$fields[$field][$file['path'] ?? $file['name']] = $file;
}
diff --git a/system/src/Grav/Common/GPM/AbstractCollection.php b/system/src/Grav/Common/GPM/AbstractCollection.php
index a9e1ac97..b5b867ef 100644
--- a/system/src/Grav/Common/GPM/AbstractCollection.php
+++ b/system/src/Grav/Common/GPM/AbstractCollection.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,13 +11,23 @@ namespace Grav\Common\GPM;
use Grav\Common\Iterator;
+/**
+ * Class AbstractCollection
+ * @package Grav\Common\GPM
+ */
abstract class AbstractCollection extends Iterator
{
+ /**
+ * @return string
+ */
public function toJson()
{
- return json_encode($this->toArray());
+ return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
}
+ /**
+ * @return array
+ */
public function toArray()
{
$items = [];
diff --git a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php
index bc4ecab9..5d6ad9fb 100644
--- a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php
+++ b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,10 +11,18 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Iterator;
+/**
+ * Class AbstractPackageCollection
+ * @package Grav\Common\GPM\Common
+ */
abstract class AbstractPackageCollection extends Iterator
{
+ /** @var string */
protected $type;
+ /**
+ * @return string
+ */
public function toJson()
{
$items = [];
@@ -23,9 +31,12 @@ abstract class AbstractPackageCollection extends Iterator
$items[$name] = $package->toArray();
}
- return json_encode($items);
+ return json_encode($items, JSON_THROW_ON_ERROR);
}
+ /**
+ * @return array
+ */
public function toArray()
{
$items = [];
diff --git a/system/src/Grav/Common/GPM/Common/CachedCollection.php b/system/src/Grav/Common/GPM/Common/CachedCollection.php
index a6fca4db..172ba939 100644
--- a/system/src/Grav/Common/GPM/Common/CachedCollection.php
+++ b/system/src/Grav/Common/GPM/Common/CachedCollection.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,10 +11,20 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Iterator;
+/**
+ * Class CachedCollection
+ * @package Grav\Common\GPM\Common
+ */
class CachedCollection extends Iterator
{
- protected static $cache;
-
+ /** @var array */
+ protected static $cache = [];
+
+ /**
+ * CachedCollection constructor.
+ *
+ * @param array $items
+ */
public function __construct($items)
{
parent::__construct();
diff --git a/system/src/Grav/Common/GPM/Common/Package.php b/system/src/Grav/Common/GPM/Common/Package.php
index 40d37591..e06ebb60 100644
--- a/system/src/Grav/Common/GPM/Common/Package.php
+++ b/system/src/Grav/Common/GPM/Common/Package.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,13 +11,19 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Data\Data;
+/**
+ * @property string $name
+ */
class Package
{
- /**
- * @var Data
- */
+ /** @var Data */
protected $data;
+ /**
+ * Package constructor.
+ * @param Data $package
+ * @param string|null $type
+ */
public function __construct(Data $package, $type = null)
{
$this->data = $package;
@@ -35,26 +41,45 @@ class Package
return $this->data;
}
+ /**
+ * @param string $key
+ * @return mixed
+ */
public function __get($key)
{
return $this->data->get($key);
}
+ /**
+ * @param string $key
+ * @param mixed $value
+ * @return void
+ */
public function __set($key, $value)
{
- return $this->data->set($key, $value);
+ $this->data->set($key, $value);
}
+ /**
+ * @param string $key
+ * @return bool
+ */
public function __isset($key)
{
return isset($this->data->{$key});
}
+ /**
+ * @return string
+ */
public function __toString()
{
return $this->toJson();
}
+ /**
+ * @return string
+ */
public function toJson()
{
return $this->data->toJson();
diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php
index 6c9e7b0e..45056963 100644
--- a/system/src/Grav/Common/GPM/GPM.php
+++ b/system/src/Grav/Common/GPM/GPM.php
@@ -3,44 +3,47 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
+use Exception;
use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Inflector;
use Grav\Common\Iterator;
use Grav\Common\Utils;
use RocketTheme\Toolbox\File\YamlFile;
+use RuntimeException;
+use stdClass;
+use function array_key_exists;
+use function count;
+use function in_array;
+use function is_array;
+use function is_object;
+/**
+ * Class GPM
+ * @package Grav\Common\GPM
+ */
class GPM extends Iterator
{
- /**
- * Local installed Packages
- * @var Local\Packages
- */
+ /** @var Local\Packages Local installed Packages */
private $installed;
-
- /**
- * Remote available Packages
- * @var Remote\Packages
- */
+ /** @var Remote\Packages|null Remote available Packages */
private $repository;
-
- /**
- * @var Remote\GravCore
- */
- public $grav;
-
- /**
- * Internal cache
- * @var array
- */
+ /** @var Remote\GravCore|null Remove Grav Packages */
+ private $grav;
+ /** @var bool */
+ private $refresh;
+ /** @var callable|null */
+ private $callback;
+
+ /** @var array Internal cache */
protected $cache;
-
+ /** @var array */
protected $install_paths = [
'plugins' => 'user/plugins/%name%',
'themes' => 'user/themes/%name%',
@@ -49,19 +52,52 @@ class GPM extends Iterator
/**
* Creates a new GPM instance with Local and Remote packages available
+ *
* @param bool $refresh Applies to Remote Packages only and forces a refetch of data
- * @param callable $callback Either a function or callback in array notation
+ * @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
{
parent::__construct();
+
+ Folder::create(CACHE_DIR . '/gpm');
+
$this->cache = [];
$this->installed = new Local\Packages();
- try {
- $this->repository = new Remote\Packages($refresh, $callback);
- $this->grav = new Remote\GravCore($refresh, $callback);
- } catch (\Exception $e) {
+ $this->refresh = $refresh;
+ $this->callback = $callback;
+ }
+
+ /**
+ * Magic getter method
+ *
+ * @param string $offset Asset name value
+ * @return mixed Asset value
+ */
+ public function __get($offset)
+ {
+ switch ($offset) {
+ case 'grav':
+ return $this->getGrav();
}
+
+ return parent::__get($offset);
+ }
+
+ /**
+ * Magic method to determine if the attribute is set
+ *
+ * @param string $offset Asset name value
+ * @return bool True if the value is set
+ */
+ public function __isset($offset)
+ {
+ switch ($offset) {
+ case 'grav':
+ return $this->getGrav() !== null;
+ }
+
+ return parent::__isset($offset);
}
/**
@@ -92,11 +128,13 @@ class GPM extends Iterator
$items[$type] = $to_install;
$items['total'] += count($to_install);
}
+
return $items;
}
/**
* Returns the amount of locally installed packages
+ *
* @return int Amount of installed packages
*/
public function countInstalled()
@@ -110,29 +148,22 @@ class GPM extends Iterator
* Return the instance of a specific Package
*
* @param string $slug The slug of the Package
- * @return Local\Package The instance of the Package
+ * @return Local\Package|null The instance of the Package
*/
public function getInstalledPackage($slug)
{
- if (isset($this->installed['plugins'][$slug])) {
- return $this->installed['plugins'][$slug];
- }
-
- if (isset($this->installed['themes'][$slug])) {
- return $this->installed['themes'][$slug];
- }
-
- return null;
+ return $this->getInstalledPlugin($slug) ?? $this->getInstalledTheme($slug);
}
/**
* Return the instance of a specific Plugin
+ *
* @param string $slug The slug of the Plugin
- * @return Local\Package The instance of the Plugin
+ * @return Local\Package|null The instance of the Plugin
*/
public function getInstalledPlugin($slug)
{
- return $this->installed['plugins'][$slug];
+ return $this->installed['plugins'][$slug] ?? null;
}
/**
@@ -144,33 +175,56 @@ class GPM extends Iterator
return $this->installed['plugins'];
}
+
+ /**
+ * Returns the plugin's enabled state
+ *
+ * @param string $slug
+ * @return bool True if the Plugin is Enabled. False if manually set to enable:false. Null otherwise.
+ */
+ public function isPluginEnabled($slug): bool
+ {
+ $grav = Grav::instance();
+
+ return ($grav['config']['plugins'][$slug]['enabled'] ?? false) === true;
+ }
+
/**
* Checks if a Plugin is installed
+ *
* @param string $slug The slug of the Plugin
* @return bool True if the Plugin has been installed. False otherwise
*/
- public function isPluginInstalled($slug)
+ public function isPluginInstalled($slug): bool
{
return isset($this->installed['plugins'][$slug]);
}
+ /**
+ * @param string $slug
+ * @return bool
+ */
public function isPluginInstalledAsSymlink($slug)
{
- return $this->installed['plugins'][$slug]->symlink;
+ $plugin = $this->getInstalledPlugin($slug);
+
+ return (bool)($plugin->symlink ?? false);
}
/**
* Return the instance of a specific Theme
+ *
* @param string $slug The slug of the Theme
- * @return Local\Package The instance of the Theme
+ * @return Local\Package|null The instance of the Theme
*/
public function getInstalledTheme($slug)
{
- return $this->installed['themes'][$slug];
+ return $this->installed['themes'][$slug] ?? null;
}
/**
* Returns the Locally installed themes
+ *
* @return Iterator The installed themes
*/
public function getInstalledThemes()
@@ -178,40 +232,52 @@ class GPM extends Iterator
return $this->installed['themes'];
}
+ /**
+ * Checks if a Theme is enabled
+ *
+ * @param string $slug The slug of the Theme
+ * @return bool True if the Theme has been set to the default theme. False if installed, but not enabled. Null otherwise.
+ */
+ public function isThemeEnabled($slug): bool
+ {
+ $grav = Grav::instance();
+
+ $current_theme = $grav['config']['system']['pages']['theme'] ?? null;
+
+ return $current_theme === $slug;
+ }
+
/**
* Checks if a Theme is installed
+ *
* @param string $slug The slug of the Theme
* @return bool True if the Theme has been installed. False otherwise
*/
- public function isThemeInstalled($slug)
+ public function isThemeInstalled($slug): bool
{
return isset($this->installed['themes'][$slug]);
}
/**
* Returns the amount of updates available
+ *
* @return int Amount of available updates
*/
public function countUpdates()
{
- $count = 0;
-
- $count += count($this->getUpdatablePlugins());
- $count += count($this->getUpdatableThemes());
-
- return $count;
+ return count($this->getUpdatablePlugins()) + count($this->getUpdatableThemes());
}
/**
* Returns an array of Plugins and Themes that can be updated.
* Plugins and Themes are extended with the `available` property that relies to the remote version
+ *
* @param array $list_type_update specifies what type of package to update
* @return array Array of updatable Plugins and Themes.
* Format: ['total' => int, 'plugins' => array, 'themes' => array]
*/
public function getUpdatable($list_type_update = ['plugins' => true, 'themes' => true])
{
-
$items = ['total' => 0];
foreach ($list_type_update as $type => $type_updatable) {
if ($type_updatable === false) {
@@ -222,18 +288,26 @@ class GPM extends Iterator
$items[$type] = $to_update;
$items['total'] += count($to_update);
}
+
return $items;
}
/**
* Returns an array of Plugins that can be updated.
* The Plugins are extended with the `available` property that relies to the remote version
+ *
* @return array Array of updatable Plugins
*/
public function getUpdatablePlugins()
{
$items = [];
- $repository = $this->repository['plugins'];
+
+ $repository = $this->getRepository();
+ if (null === $repository) {
+ return $items;
+ }
+
+ $plugins = $repository['plugins'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@@ -241,18 +315,18 @@ class GPM extends Iterator
}
foreach ($this->installed['plugins'] as $slug => $plugin) {
- if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+ if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
- $local_version = $plugin->version ?: 'Unknown';
- $remote_version = $repository[$slug]->version;
+ $local_version = $plugin->version ?? 'Unknown';
+ $remote_version = $plugins[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
- $repository[$slug]->available = $remote_version;
- $repository[$slug]->version = $local_version;
- $repository[$slug]->type = $repository[$slug]->release_type;
- $items[$slug] = $repository[$slug];
+ $plugins[$slug]->available = $remote_version;
+ $plugins[$slug]->version = $local_version;
+ $plugins[$slug]->type = $plugins[$slug]->release_type;
+ $items[$slug] = $plugins[$slug];
}
}
@@ -265,20 +339,24 @@ class GPM extends Iterator
* Get the latest release of a package from the GPM
*
* @param string $package_name
- *
* @return string|null
*/
public function getLatestVersionOfPackage($package_name)
{
- $repository = $this->repository['plugins'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->available ?: $repository[$package_name]->version;
+ $repository = $this->getRepository();
+ if (null === $repository) {
+ return null;
+ }
+
+ $plugins = $repository['plugins'];
+ if (isset($plugins[$package_name])) {
+ return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
}
//Not a plugin, it's a theme?
- $repository = $this->repository['themes'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->available ?: $repository[$package_name]->version;
+ $themes = $repository['themes'];
+ if (isset($themes[$package_name])) {
+ return $themes[$package_name]->available ?: $themes[$package_name]->version;
}
return null;
@@ -286,6 +364,7 @@ class GPM extends Iterator
/**
* Check if a Plugin or Theme is updatable
+ *
* @param string $slug The slug of the package
* @return bool True if updatable. False otherwise or if not found
*/
@@ -296,6 +375,7 @@ class GPM extends Iterator
/**
* Checks if a Plugin is updatable
+ *
* @param string $plugin The slug of the Plugin
* @return bool True if the Plugin is updatable. False otherwise
*/
@@ -307,12 +387,19 @@ class GPM extends Iterator
/**
* Returns an array of Themes that can be updated.
* The Themes are extended with the `available` property that relies to the remote version
+ *
* @return array Array of updatable Themes
*/
public function getUpdatableThemes()
{
$items = [];
- $repository = $this->repository['themes'];
+
+ $repository = $this->getRepository();
+ if (null === $repository) {
+ return $items;
+ }
+
+ $themes = $repository['themes'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@@ -320,18 +407,18 @@ class GPM extends Iterator
}
foreach ($this->installed['themes'] as $slug => $plugin) {
- if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+ if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
- $local_version = $plugin->version ?: 'Unknown';
- $remote_version = $repository[$slug]->version;
+ $local_version = $plugin->version ?? 'Unknown';
+ $remote_version = $themes[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
- $repository[$slug]->available = $remote_version;
- $repository[$slug]->version = $local_version;
- $repository[$slug]->type = $repository[$slug]->release_type;
- $items[$slug] = $repository[$slug];
+ $themes[$slug]->available = $remote_version;
+ $themes[$slug]->version = $local_version;
+ $themes[$slug]->type = $themes[$slug]->release_type;
+ $items[$slug] = $themes[$slug];
}
}
@@ -342,6 +429,7 @@ class GPM extends Iterator
/**
* Checks if a Theme is Updatable
+ *
* @param string $theme The slug of the Theme
* @return bool True if the Theme is updatable. False otherwise
*/
@@ -354,20 +442,24 @@ class GPM extends Iterator
* Get the release type of a package (stable / testing)
*
* @param string $package_name
- *
* @return string|null
*/
public function getReleaseType($package_name)
{
- $repository = $this->repository['plugins'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->release_type;
+ $repository = $this->getRepository();
+ if (null === $repository) {
+ return null;
+ }
+
+ $plugins = $repository['plugins'];
+ if (isset($plugins[$package_name])) {
+ return $plugins[$package_name]->release_type;
}
//Not a plugin, it's a theme?
- $repository = $this->repository['themes'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->release_type;
+ $themes = $repository['themes'];
+ if (isset($themes[$package_name])) {
+ return $themes[$package_name]->release_type;
}
return null;
@@ -377,7 +469,6 @@ class GPM extends Iterator
* Returns true if the package latest release is stable
*
* @param string $package_name
- *
* @return bool
*/
public function isStableRelease($package_name)
@@ -389,81 +480,107 @@ class GPM extends Iterator
* Returns true if the package latest release is testing
*
* @param string $package_name
- *
* @return bool
*/
public function isTestingRelease($package_name)
{
- $hasTesting = isset($this->getInstalledPackage($package_name)->testing);
- $testing = $hasTesting ? $this->getInstalledPackage($package_name)->testing : false;
+ $package = $this->getInstalledPackage($package_name);
+ $testing = $package->testing ?? false;
return $this->getReleaseType($package_name) === 'testing' || $testing;
}
/**
* Returns a Plugin from the repository
+ *
* @param string $slug The slug of the Plugin
- * @return mixed Package if found, NULL if not
+ * @return Remote\Package|null Package if found, NULL if not
*/
public function getRepositoryPlugin($slug)
{
- return @$this->repository['plugins'][$slug];
+ $packages = $this->getRepositoryPlugins();
+
+ return $packages ? ($packages[$slug] ?? null) : null;
}
/**
* Returns the list of Plugins available in the repository
- * @return Iterator The Plugins remotely available
+ *
+ * @return Iterator|null The Plugins remotely available
*/
public function getRepositoryPlugins()
{
- return $this->repository['plugins'];
+ return $this->getRepository()['plugins'] ?? null;
}
/**
* Returns a Theme from the repository
+ *
* @param string $slug The slug of the Theme
- * @return mixed Package if found, NULL if not
+ * @return Remote\Package|null Package if found, NULL if not
*/
public function getRepositoryTheme($slug)
{
- return @$this->repository['themes'][$slug];
+ $packages = $this->getRepositoryThemes();
+
+ return $packages ? ($packages[$slug] ?? null) : null;
}
/**
* Returns the list of Themes available in the repository
- * @return Iterator The Themes remotely available
+ *
+ * @return Iterator|null The Themes remotely available
*/
public function getRepositoryThemes()
{
- return $this->repository['themes'];
+ return $this->getRepository()['themes'] ?? null;
}
/**
* Returns the list of Plugins and Themes available in the repository
- * @return Remote\Packages Available Plugins and Themes
+ *
+ * @return Remote\Packages|null Available Plugins and Themes
* Format: ['plugins' => array, 'themes' => array]
*/
public function getRepository()
{
+ if (null === $this->repository) {
+ try {
+ $this->repository = new Remote\Packages($this->refresh, $this->callback);
+ } catch (Exception $e) {}
+ }
+
return $this->repository;
}
+ /**
+ * Returns Grav version available in the repository
+ *
+ * @return Remote\GravCore|null
+ */
+ public function getGrav()
+ {
+ if (null === $this->grav) {
+ try {
+ $this->grav = new Remote\GravCore($this->refresh, $this->callback);
+ } catch (Exception $e) {}
+ }
+
+ return $this->grav;
+ }
+
/**
* Searches for a Package in the repository
+ *
* @param string $search Can be either the slug or the name
* @param bool $ignore_exception True if should not fire an exception (for use in Twig)
- * @return Remote\Package|bool Package if found, FALSE if not
+ * @return Remote\Package|false Package if found, FALSE if not
*/
public function findPackage($search, $ignore_exception = false)
{
$search = strtolower($search);
- $found = $this->getRepositoryTheme($search);
- if ($found) {
- return $found;
- }
-
- $found = $this->getRepositoryPlugin($search);
+ $found = $this->getRepositoryPlugin($search) ?? $this->getRepositoryTheme($search);
if ($found) {
return $found;
}
@@ -471,31 +588,27 @@ class GPM extends Iterator
$themes = $this->getRepositoryThemes();
$plugins = $this->getRepositoryPlugins();
- if (!$themes && !$plugins) {
- if (!is_writable(ROOT_DIR . '/cache/gpm')) {
- throw new \RuntimeException("The cache/gpm folder is not writable. Please check the folder permissions.");
+ if (null === $themes || null === $plugins) {
+ if (!is_writable(GRAV_ROOT . '/cache/gpm')) {
+ throw new RuntimeException('The cache/gpm folder is not writable. Please check the folder permissions.');
}
if ($ignore_exception) {
return false;
}
- throw new \RuntimeException("GPM not reachable. Please check your internet connection or check the Grav site is reachable");
+ throw new RuntimeException('GPM not reachable. Please check your internet connection or check the Grav site is reachable');
}
- if ($themes) {
- foreach ($themes as $slug => $theme) {
- if ($search == $slug || $search == $theme->name) {
- return $theme;
- }
+ foreach ($themes as $slug => $theme) {
+ if ($search === $slug || $search === $theme->name) {
+ return $theme;
}
}
- if ($plugins) {
- foreach ($plugins as $slug => $plugin) {
- if ($search == $slug || $search == $plugin->name) {
- return $plugin;
- }
+ foreach ($plugins as $slug => $plugin) {
+ if ($search === $slug || $search === $plugin->name) {
+ return $plugin;
}
}
@@ -507,15 +620,19 @@ class GPM extends Iterator
*
* @param string $package_file
* @param string $tmp
- * @return null|string
+ * @return string|null
*/
public static function downloadPackage($package_file, $tmp)
{
$package = parse_url($package_file);
- $filename = basename($package['path']);
+ if (!is_array($package)) {
+ throw new \RuntimeException("Malformed GPM URL: {$package_file}");
+ }
+
+ $filename = basename($package['path'] ?? '');
- if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && $package['host'] !== 'getgrav.org') {
- throw new \RuntimeException("Only official GPM URLs are allowed. You can modify this behavior in the System configuration.");
+ if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && ($package['host'] ?? null) !== 'getgrav.org') {
+ throw new RuntimeException('Only official GPM URLs are allowed. You can modify this behavior in the System configuration.');
}
$output = Response::get($package_file, []);
@@ -534,16 +651,16 @@ class GPM extends Iterator
*
* @param string $package_file
* @param string $tmp
- * @return null|string
+ * @return string|null
*/
public static function copyPackage($package_file, $tmp)
{
$package_file = realpath($package_file);
- if (file_exists($package_file)) {
+ if ($package_file && file_exists($package_file)) {
$filename = basename($package_file);
Folder::create($tmp);
- copy(realpath($package_file), $tmp . DS . $filename);
+ copy($package_file, $tmp . DS . $filename);
return $tmp . DS . $filename;
}
@@ -554,15 +671,14 @@ class GPM extends Iterator
* Try to guess the package type from the source files
*
* @param string $source
- * @return bool|string
+ * @return string|false
*/
public static function getPackageType($source)
{
$plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m';
$theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m';
- if (
- file_exists($source . 'system/defines.php') &&
+ if (file_exists($source . 'system/defines.php') &&
file_exists($source . 'system/config/system.yaml')
) {
return 'grav';
@@ -581,8 +697,13 @@ class GPM extends Iterator
if (Utils::contains($name, 'plugin')) {
return 'plugin';
}
- foreach (glob($source . '*.php') as $filename) {
+
+ $glob = glob($source . '*.php') ?: [];
+ foreach ($glob as $filename) {
$contents = file_get_contents($filename);
+ if (!$contents) {
+ continue;
+ }
if (preg_match($theme_regex, $contents)) {
return 'theme';
}
@@ -599,19 +720,22 @@ class GPM extends Iterator
* Try to guess the package name from the source files
*
* @param string $source
- * @return bool|string
+ * @return string|false
*/
public static function getPackageName($source)
{
$ignore_yaml_files = ['blueprints', 'languages'];
- foreach (glob($source . '*.yaml') as $filename) {
+ $glob = glob($source . '*.yaml') ?: [];
+ foreach ($glob as $filename) {
$name = strtolower(basename($filename, '.yaml'));
if (in_array($name, $ignore_yaml_files)) {
continue;
}
+
return $name;
}
+
return false;
}
@@ -619,7 +743,7 @@ class GPM extends Iterator
* Find/Parse the blueprint file
*
* @param string $source
- * @return array|bool
+ * @return array|false
*/
public static function getBlueprints($source)
{
@@ -651,11 +775,13 @@ class GPM extends Iterator
} else {
$install_path = $locator->findResource('plugins://', false) . DS . $name;
}
+
return $install_path;
}
/**
* Searches for a list of Packages in the repository
+ *
* @param array $searches An array of either slugs or names
* @return array Array of found Packages
* Format: ['total' => int, 'not_found' => array, ]
@@ -695,7 +821,7 @@ class GPM extends Iterator
$type = 'plugins';
}
- $not_found = new \stdClass();
+ $not_found = new stdClass();
$not_found->name = $inflector::camelize($search);
$not_found->slug = $search;
$not_found->package_type = $type;
@@ -712,7 +838,6 @@ class GPM extends Iterator
* Return the list of packages that have the passed one as dependency
*
* @param string $slug The slug name of the package
- *
* @return array
*/
public function getPackagesThatDependOnPackage($slug)
@@ -721,23 +846,21 @@ class GPM extends Iterator
$themes = $this->getInstalledThemes();
$packages = array_merge($plugins->toArray(), $themes->toArray());
- $dependent_packages = [];
-
+ $list = [];
foreach ($packages as $package_name => $package) {
- if (isset($package['dependencies'])) {
- foreach ($package['dependencies'] as $dependency) {
- if (is_array($dependency) && isset($dependency['name'])) {
- $dependency = $dependency['name'];
- }
+ $dependencies = $package['dependencies'] ?? [];
+ foreach ($dependencies as $dependency) {
+ if (is_array($dependency) && isset($dependency['name'])) {
+ $dependency = $dependency['name'];
+ }
- if ($dependency === $slug) {
- $dependent_packages[] = $package_name;
- }
+ if ($dependency === $slug) {
+ $list[] = $package_name;
}
}
}
- return $dependent_packages;
+ return $list;
}
@@ -746,12 +869,11 @@ class GPM extends Iterator
*
* @param string $package_slug
* @param string $dependency_slug
- *
- * @return mixed
+ * @return mixed|null
*/
public function getVersionOfDependencyRequiredByPackage($package_slug, $dependency_slug)
{
- $dependencies = $this->getInstalledPackage($package_slug)->dependencies;
+ $dependencies = $this->getInstalledPackage($package_slug)->dependencies ?? [];
foreach ($dependencies as $dependency) {
if (isset($dependency[$dependency_slug])) {
return $dependency[$dependency_slug];
@@ -768,35 +890,28 @@ class GPM extends Iterator
* @param string $slug
* @param string $version_with_operator
* @param array $ignore_packages_list
- *
* @return bool
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
- public function checkNoOtherPackageNeedsThisDependencyInALowerVersion(
- $slug,
- $version_with_operator,
- $ignore_packages_list
- ) {
-
+ public function checkNoOtherPackageNeedsThisDependencyInALowerVersion($slug, $version_with_operator, $ignore_packages_list)
+ {
// check if any of the currently installed package need this in a lower version than the one we need. In case, abort and tell which package
$dependent_packages = $this->getPackagesThatDependOnPackage($slug);
$version = $this->calculateVersionNumberFromDependencyVersion($version_with_operator);
if (count($dependent_packages)) {
foreach ($dependent_packages as $dependent_package) {
- $other_dependency_version_with_operator = $this->getVersionOfDependencyRequiredByPackage($dependent_package,
- $slug);
+ $other_dependency_version_with_operator = $this->getVersionOfDependencyRequiredByPackage($dependent_package, $slug);
$other_dependency_version = $this->calculateVersionNumberFromDependencyVersion($other_dependency_version_with_operator);
// check version is compatible with the one needed by the current package
if ($this->versionFormatIsNextSignificantRelease($other_dependency_version_with_operator)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($version,
- $other_dependency_version);
- if (!$compatible) {
- if (!in_array($dependent_package, $ignore_packages_list, true)) {
- throw new \RuntimeException("Package $slug is required in an older version by package $dependent_package . This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug .",
- 2);
- }
+ $compatible = $this->checkNextSignificantReleasesAreCompatible($version, $other_dependency_version);
+ if (!$compatible && !in_array($dependent_package, $ignore_packages_list, true)) {
+ throw new RuntimeException(
+ "Package $slug is required in an older version by package $dependent_package . This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug .",
+ 2
+ );
}
}
}
@@ -809,14 +924,14 @@ class GPM extends Iterator
* Check the passed packages list can be updated
*
* @param array $packages_names_list
- *
- * @throws \Exception
+ * @return void
+ * @throws Exception
*/
public function checkPackagesCanBeInstalled($packages_names_list)
{
foreach ($packages_names_list as $package_name) {
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name,
- $this->getLatestVersionOfPackage($package_name), $packages_names_list);
+ $latest = $this->getLatestVersionOfPackage($package_name);
+ $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name, $latest, $packages_names_list);
}
}
@@ -829,27 +944,25 @@ class GPM extends Iterator
* `update` means the package is already installed and must be updated as a dependency needs a higher version.
*
* @param array $packages
- *
- * @return mixed
- * @throws \Exception
+ * @return array
+ * @throws RuntimeException
*/
public function getDependencies($packages)
{
$dependencies = $this->calculateMergedDependenciesOfPackages($packages);
foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) {
- if (\in_array($dependency_slug, $packages, true)) {
+ $dependency_slug = (string)$dependency_slug;
+ if (in_array($dependency_slug, $packages, true)) {
unset($dependencies[$dependency_slug]);
continue;
}
// Check PHP version
if ($dependency_slug === 'php') {
- $current_php_version = phpversion();
- if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
- $current_php_version) === 1
- ) {
+ $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
+ if (version_compare($testVersion, PHP_VERSION) === 1) {
//Needs a Grav update first
- throw new \RuntimeException("One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this");
+ throw new RuntimeException("One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this");
}
unset($dependencies[$dependency_slug]);
@@ -858,11 +971,10 @@ class GPM extends Iterator
//First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell.
if ($dependency_slug === 'grav') {
- if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
- GRAV_VERSION) === 1
- ) {
+ $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
+ if (version_compare($testVersion, GRAV_VERSION) === 1) {
//Needs a Grav update first
- throw new \RuntimeException("One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release.");
+ throw new RuntimeException("One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release.");
}
unset($dependencies[$dependency_slug]);
@@ -886,15 +998,15 @@ class GPM extends Iterator
$currentlyInstalledVersion = $package_yaml['version'];
// if requirement is next significant release, check is compatible with currently installed version, might not be
- if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
- if ($this->firstVersionIsLower($dependencyVersion, $currentlyInstalledVersion)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
- $currentlyInstalledVersion);
+ if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)
+ && $this->firstVersionIsLower($dependencyVersion, $currentlyInstalledVersion)) {
+ $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion, $currentlyInstalledVersion);
- if (!$compatible) {
- throw new \RuntimeException('Dependency ' . $dependency_slug . ' is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.',
- 2);
- }
+ if (!$compatible) {
+ throw new RuntimeException(
+ 'Dependency ' . $dependency_slug . ' is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.',
+ 2
+ );
}
}
@@ -903,19 +1015,19 @@ class GPM extends Iterator
if ($this->firstVersionIsLower($latestRelease, $dependencyVersion)) {
//throw an exception if a required version cannot be found in the GPM yet
- throw new \RuntimeException('Dependency ' . $package_yaml['name'] . ' is required in version ' . $dependencyVersion . ' which is higher than the latest release, ' . $latestRelease . ' . Try running `bin/gpm -f index` to force a refresh of the GPM cache',
- 1);
+ throw new RuntimeException(
+ 'Dependency ' . $package_yaml['name'] . ' is required in version ' . $dependencyVersion . ' which is higher than the latest release, ' . $latestRelease . ' . Try running `bin/gpm -f index` to force a refresh of the GPM cache',
+ 1
+ );
}
if ($this->firstVersionIsLower($currentlyInstalledVersion, $dependencyVersion)) {
$dependencies[$dependency_slug] = 'update';
+ } elseif ($currentlyInstalledVersion === $latestRelease) {
+ unset($dependencies[$dependency_slug]);
} else {
- if ($currentlyInstalledVersion == $latestRelease) {
- unset($dependencies[$dependency_slug]);
- } else {
- // an update is not strictly required mark as 'ignore'
- $dependencies[$dependency_slug] = 'ignore';
- }
+ // an update is not strictly required mark as 'ignore'
+ $dependencies[$dependency_slug] = 'ignore';
}
} else {
$dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
@@ -924,12 +1036,16 @@ class GPM extends Iterator
if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
$latestVersionOfPackage = $this->getLatestVersionOfPackage($dependency_slug);
if ($this->firstVersionIsLower($dependencyVersion, $latestVersionOfPackage)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
- $latestVersionOfPackage);
+ $compatible = $this->checkNextSignificantReleasesAreCompatible(
+ $dependencyVersion,
+ $latestVersionOfPackage
+ );
if (!$compatible) {
- throw new \Exception('Dependency ' . $dependency_slug . ' is required in an older version than the latest release available, and it cannot be installed. This package must be updated. Please get in touch with its developer.',
- 2);
+ throw new RuntimeException(
+ 'Dependency ' . $dependency_slug . ' is required in an older version than the latest release available, and it cannot be installed. This package must be updated. Please get in touch with its developer.',
+ 2
+ );
}
}
}
@@ -944,14 +1060,26 @@ class GPM extends Iterator
return $dependencies;
}
+ /**
+ * @param array $dependencies_slugs
+ * @return void
+ */
public function checkNoOtherPackageNeedsTheseDependenciesInALowerVersion($dependencies_slugs)
{
foreach ($dependencies_slugs as $dependency_slug) {
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($dependency_slug,
- $this->getLatestVersionOfPackage($dependency_slug), $dependencies_slugs);
+ $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion(
+ $dependency_slug,
+ $this->getLatestVersionOfPackage($dependency_slug),
+ $dependencies_slugs
+ );
}
}
+ /**
+ * @param string $firstVersion
+ * @param string $secondVersion
+ * @return bool
+ */
private function firstVersionIsLower($firstVersion, $secondVersion)
{
return version_compare($firstVersion, $secondVersion) === -1;
@@ -961,83 +1089,69 @@ class GPM extends Iterator
* Calculates and merges the dependencies of a package
*
* @param string $packageName The package information
- *
* @param array $dependencies The dependencies array
- *
* @return array
- * @throws \Exception
*/
private function calculateMergedDependenciesOfPackage($packageName, $dependencies)
{
$packageData = $this->findPackage($packageName);
- //Check for dependencies
- if (isset($packageData->dependencies)) {
- foreach ($packageData->dependencies as $dependency) {
- $current_package_name = $dependency['name'];
- if (isset($dependency['version'])) {
- $current_package_version_information = $dependency['version'];
- }
+ if (empty($packageData->dependencies)) {
+ return $dependencies;
+ }
- if (!isset($dependencies[$current_package_name])) {
- // Dependency added for the first time
+ foreach ($packageData->dependencies as $dependency) {
+ $dependencyName = $dependency['name'] ?? null;
+ if (!$dependencyName) {
+ continue;
+ }
- if (!isset($current_package_version_information)) {
- $dependencies[$current_package_name] = '*';
- } else {
- $dependencies[$current_package_name] = $current_package_version_information;
- }
+ $dependencyVersion = $dependency['version'] ?? '*';
- //Factor in the package dependencies too
- $dependencies = $this->calculateMergedDependenciesOfPackage($current_package_name, $dependencies);
- } else {
- // Dependency already added by another package
- //if this package requires a version higher than the currently stored one, store this requirement instead
- if (isset($current_package_version_information) && $current_package_version_information !== '*') {
+ if (!isset($dependencies[$dependencyName])) {
+ // Dependency added for the first time
+ $dependencies[$dependencyName] = $dependencyVersion;
- $currently_stored_version_information = $dependencies[$current_package_name];
- $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currently_stored_version_information);
+ //Factor in the package dependencies too
+ $dependencies = $this->calculateMergedDependenciesOfPackage($dependencyName, $dependencies);
+ } elseif ($dependencyVersion !== '*') {
+ // Dependency already added by another package
+ // If this package requires a version higher than the currently stored one, store this requirement instead
+ $currentDependencyVersion = $dependencies[$dependencyName];
+ $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currentDependencyVersion);
- $currently_stored_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($currently_stored_version_information)) {
- $currently_stored_version_is_in_next_significant_release_format = true;
- }
+ $currently_stored_version_is_in_next_significant_release_format = false;
+ if ($this->versionFormatIsNextSignificantRelease($currentDependencyVersion)) {
+ $currently_stored_version_is_in_next_significant_release_format = true;
+ }
- if (!$currently_stored_version_number) {
- $currently_stored_version_number = '*';
- }
+ if (!$currently_stored_version_number) {
+ $currently_stored_version_number = '*';
+ }
- $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($current_package_version_information);
- if (!$current_package_version_number) {
- throw new \RuntimeException('Bad format for version of dependency ' . $current_package_name . ' for package ' . $packageName,
- 1);
- }
+ $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($dependencyVersion);
+ if (!$current_package_version_number) {
+ throw new RuntimeException("Bad format for version of dependency {$dependencyName} for package {$packageName}", 1);
+ }
- $current_package_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($current_package_version_information)) {
- $current_package_version_is_in_next_significant_release_format = true;
- }
+ $current_package_version_is_in_next_significant_release_format = false;
+ if ($this->versionFormatIsNextSignificantRelease($dependencyVersion)) {
+ $current_package_version_is_in_next_significant_release_format = true;
+ }
- //If I had stored '*', change right away with the more specific version required
- if ($currently_stored_version_number === '*') {
- $dependencies[$current_package_name] = $current_package_version_information;
- } else {
- if (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) {
- //Comparing versions equals or higher, a simple version_compare is enough
- if (version_compare($currently_stored_version_number,
- $current_package_version_number) === -1
- ) { //Current package version is higher
- $dependencies[$current_package_name] = $current_package_version_information;
- }
- } else {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number,
- $current_package_version_number);
- if (!$compatible) {
- throw new \RuntimeException('Dependency ' . $current_package_name . ' is required in two incompatible versions',
- 2);
- }
- }
- }
+ //If I had stored '*', change right away with the more specific version required
+ if ($currently_stored_version_number === '*') {
+ $dependencies[$dependencyName] = $dependencyVersion;
+ } elseif (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) {
+ //Comparing versions equals or higher, a simple version_compare is enough
+ if (version_compare($currently_stored_version_number, $current_package_version_number) === -1) {
+ //Current package version is higher
+ $dependencies[$dependencyName] = $dependencyVersion;
+ }
+ } else {
+ $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number, $current_package_version_number);
+ if (!$compatible) {
+ throw new RuntimeException("Dependency {$dependencyName} is required in two incompatible versions", 2);
}
}
}
@@ -1050,9 +1164,7 @@ class GPM extends Iterator
* Calculates and merges the dependencies of the passed packages
*
* @param array $packages
- *
- * @return mixed
- * @throws \Exception
+ * @return array
*/
public function calculateMergedDependenciesOfPackages($packages)
{
@@ -1075,8 +1187,7 @@ class GPM extends Iterator
* $versionInformation == '' => returns null
*
* @param string $version
- *
- * @return null|string
+ * @return string|null
*/
public function calculateVersionNumberFromDependencyVersion($version)
{
@@ -1102,7 +1213,6 @@ class GPM extends Iterator
* Example: returns true for $version: '~2.0'
*
* @param string $version
- *
* @return bool
*/
public function versionFormatIsNextSignificantRelease($version): bool
@@ -1116,7 +1226,6 @@ class GPM extends Iterator
* Example: returns true for $version: '>=2.0'
*
* @param string $version
- *
* @return bool
*/
public function versionFormatIsEqualOrHigher($version): bool
@@ -1134,7 +1243,6 @@ class GPM extends Iterator
*
* @param string $version1 the version string (e.g. '2.0.0' or '1.0')
* @param string $version2 the version string (e.g. '2.0.0' or '1.0')
- *
* @return bool
*/
public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool
@@ -1142,13 +1250,13 @@ class GPM extends Iterator
$version1array = explode('.', $version1);
$version2array = explode('.', $version2);
- if (\count($version1array) > \count($version2array)) {
- list($version1array, $version2array) = [$version2array, $version1array];
+ if (count($version1array) > count($version2array)) {
+ [$version1array, $version2array] = [$version2array, $version1array];
}
$i = 0;
- while ($i < \count($version1array) - 1) {
- if ($version1array[$i] != $version2array[$i]) {
+ while ($i < count($version1array) - 1) {
+ if ($version1array[$i] !== $version2array[$i]) {
return false;
}
$i++;
@@ -1156,5 +1264,4 @@ class GPM extends Iterator
return true;
}
-
}
diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php
index cd511638..639240be 100644
--- a/system/src/Grav/Common/GPM/Installer.php
+++ b/system/src/Grav/Common/GPM/Installer.php
@@ -3,15 +3,25 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
+use DirectoryIterator;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
+use RuntimeException;
+use ZipArchive;
+use function count;
+use function in_array;
+use function is_string;
+/**
+ * Class Installer
+ * @package Grav\Common\GPM
+ */
class Installer
{
/** @const No error */
@@ -33,31 +43,19 @@ class Installer
/** @const Invalid source file */
public const INVALID_SOURCE = 128;
- /**
- * Destination folder on which validation checks are applied
- * @var string
- */
+ /** @var string Destination folder on which validation checks are applied */
protected static $target;
- /**
- * @var int Error Code
- */
+ /** @var int|string Error code or string */
protected static $error = 0;
- /**
- * @var int Zip Error Code
- */
+ /** @var int Zip Error Code */
protected static $error_zip = 0;
- /**
- * @var string Post install message
- */
+ /** @var string Post install message */
protected static $message = '';
- /**
- * Default options for the install
- * @var array
- */
+ /** @var array Default options for the install */
protected static $options = [
'overwrite' => true,
'ignore_symlinks' => true,
@@ -74,7 +72,7 @@ class Installer
* @param string $zip the local path to ZIP package
* @param string $destination The local path to the Grav Instance
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
- * @param string $extracted The local path to the extacted ZIP package
+ * @param string|null $extracted The local path to the extacted ZIP package
* @param bool $keepExtracted True if you want to keep the original files
* @return bool True if everything went fine, False otherwise.
*/
@@ -84,8 +82,10 @@ class Installer
$options = array_merge(self::$options, $options);
$install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS);
- if (!self::isGravInstance($destination) || !self::isValidDestination($install_path,
- $options['exclude_checks'])
+ if (!self::isGravInstance($destination) || !self::isValidDestination(
+ $install_path,
+ $options['exclude_checks']
+ )
) {
return false;
}
@@ -135,7 +135,10 @@ class Installer
}
if (!$options['sophisticated']) {
- if ($options['theme']) {
+ $isTheme = $options['theme'] ?? false;
+ // Make sure that themes are always being copied, even if option was not set!
+ $isTheme = $isTheme || preg_match('|/themes/[^/]+|ui', $install_path);
+ if ($isTheme) {
self::copyInstall($extracted, $install_path);
} else {
self::moveInstall($extracted, $install_path);
@@ -160,7 +163,6 @@ class Installer
self::$error = self::OK;
return true;
-
}
/**
@@ -168,11 +170,11 @@ class Installer
*
* @param string $zip_file
* @param string $destination
- * @return bool|string
+ * @return string|false
*/
public static function unZip($zip_file, $destination)
{
- $zip = new \ZipArchive();
+ $zip = new ZipArchive();
$archive = $zip->open($zip_file);
if ($archive === true) {
@@ -188,7 +190,11 @@ class Installer
return false;
}
- $package_folder_name = preg_replace('#\./$#', '', $zip->getNameIndex(0));
+ $package_folder_name = $zip->getNameIndex(0);
+ if ($package_folder_name === false) {
+ throw new \RuntimeException('Bad package file: ' . basename($zip_file));
+ }
+ $package_folder_name = preg_replace('#\./$#', '', $package_folder_name);
$zip->close();
return $destination . '/' . $package_folder_name;
@@ -196,6 +202,7 @@ class Installer
self::$error = self::ZIP_EXTRACT_ERROR;
self::$error_zip = $archive;
+
return false;
}
@@ -204,23 +211,20 @@ class Installer
*
* @param string $installer_file_folder The folder path that contains install.php
* @param bool $is_install True if install, false if removal
- *
- * @return null|string
+ * @return string|null
*/
private static function loadInstaller($installer_file_folder, $is_install)
{
- $installer = null;
-
$installer_file_folder = rtrim($installer_file_folder, DS);
$install_file = $installer_file_folder . DS . 'install.php';
- if (file_exists($install_file)) {
- require_once $install_file;
- } else {
+ if (!file_exists($install_file)) {
return null;
}
+ require_once $install_file;
+
if ($is_install) {
$slug = '';
if (($pos = strpos($installer_file_folder, 'grav-plugin-')) !== false) {
@@ -243,19 +247,18 @@ class Installer
return $class_name;
}
- $class_name_alphanumeric = preg_replace('/[^a-zA-Z0-9]+/', '', $class_name);
+ $class_name_alphanumeric = preg_replace('/[^a-zA-Z0-9]+/', '', $class_name) ?? $class_name;
if (class_exists($class_name_alphanumeric)) {
return $class_name_alphanumeric;
}
- return $installer;
+ return null;
}
/**
* @param string $source_path
* @param string $install_path
- *
* @return bool
*/
public static function moveInstall($source_path, $install_path)
@@ -272,13 +275,12 @@ class Installer
/**
* @param string $source_path
* @param string $install_path
- *
* @return bool
*/
public static function copyInstall($source_path, $install_path)
{
if (empty($source_path)) {
- throw new \RuntimeException("Directory $source_path is missing");
+ throw new RuntimeException("Directory $source_path is missing");
}
Folder::rcopy($source_path, $install_path);
@@ -291,14 +293,12 @@ class Installer
* @param string $install_path
* @param array $ignores
* @param bool $keep_source
- *
* @return bool
*/
public static function sophisticatedInstall($source_path, $install_path, $ignores = [], $keep_source = false)
{
- foreach (new \DirectoryIterator($source_path) as $file) {
-
- if ($file->isLink() || $file->isDot() || \in_array($file->getFilename(), $ignores, true)) {
+ foreach (new DirectoryIterator($source_path) as $file) {
+ if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores, true)) {
continue;
}
@@ -313,7 +313,8 @@ class Installer
}
if ($file->getFilename() === 'bin') {
- foreach (glob($path . DS . '*') as $bin_file) {
+ $glob = glob($path . DS . '*') ?: [];
+ foreach ($glob as $bin_file) {
@chmod($bin_file, 0755);
}
}
@@ -331,7 +332,6 @@ class Installer
*
* @param string $path The slug of the package(s)
* @param array $options Options to use for uninstalling
- *
* @return bool True if everything went fine, False otherwise.
*/
public static function uninstall($path, $options = [])
@@ -373,7 +373,6 @@ class Installer
*
* @param string $destination The directory to run validations at
* @param array $exclude An array of constants to exclude from the validation
- *
* @return bool True if validation passed. False otherwise
*/
public static function isValidDestination($destination, $exclude = [])
@@ -391,7 +390,7 @@ class Installer
self::$error = self::NOT_DIRECTORY;
}
- if (\count($exclude) && \in_array(self::$error, $exclude, true)) {
+ if (count($exclude) && in_array(self::$error, $exclude, true)) {
return true;
}
@@ -402,7 +401,6 @@ class Installer
* Validates if the given path is a Grav Instance
*
* @param string $target The local path to the Grav Instance
- *
* @return bool True if is a Grav Instance. False otherwise
*/
public static function isGravInstance($target)
@@ -410,8 +408,7 @@ class Installer
self::$error = 0;
self::$target = $target;
- if (
- !file_exists($target . DS . 'index.php') ||
+ if (!file_exists($target . DS . 'index.php') ||
!file_exists($target . DS . 'bin') ||
!file_exists($target . DS . 'user') ||
!file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml')
@@ -424,6 +421,7 @@ class Installer
/**
* Returns the last message added by the installer
+ *
* @return string The message
*/
public static function getMessage()
@@ -433,6 +431,7 @@ class Installer
/**
* Returns the last error occurred in a string message format
+ *
* @return string The message of the last error
*/
public static function lastErrorMsg()
@@ -473,36 +472,36 @@ class Installer
case self::ZIP_EXTRACT_ERROR:
$msg = 'Unable to extract the package. ';
if (self::$error_zip) {
- switch(self::$error_zip) {
- case \ZipArchive::ER_EXISTS:
+ switch (self::$error_zip) {
+ case ZipArchive::ER_EXISTS:
$msg .= 'File already exists.';
break;
- case \ZipArchive::ER_INCONS:
+ case ZipArchive::ER_INCONS:
$msg .= 'Zip archive inconsistent.';
break;
- case \ZipArchive::ER_MEMORY:
+ case ZipArchive::ER_MEMORY:
$msg .= 'Memory allocation failure.';
break;
- case \ZipArchive::ER_NOENT:
+ case ZipArchive::ER_NOENT:
$msg .= 'No such file.';
break;
- case \ZipArchive::ER_NOZIP:
+ case ZipArchive::ER_NOZIP:
$msg .= 'Not a zip archive.';
break;
- case \ZipArchive::ER_OPEN:
+ case ZipArchive::ER_OPEN:
$msg .= "Can't open file.";
break;
- case \ZipArchive::ER_READ:
+ case ZipArchive::ER_READ:
$msg .= 'Read error.';
break;
- case \ZipArchive::ER_SEEK:
+ case ZipArchive::ER_SEEK:
$msg .= 'Seek error.';
break;
}
@@ -523,6 +522,7 @@ class Installer
/**
* Returns the last error code of the occurred error
+ *
* @return int|string The code of the last error
*/
public static function lastErrorCode()
@@ -534,8 +534,8 @@ class Installer
* Allows to manually set an error
*
* @param int|string $error the Error code
+ * @return void
*/
-
public static function setError($error)
{
self::$error = $error;
diff --git a/system/src/Grav/Common/GPM/Licenses.php b/system/src/Grav/Common/GPM/Licenses.php
index 14c92583..825343dd 100644
--- a/system/src/Grav/Common/GPM/Licenses.php
+++ b/system/src/Grav/Common/GPM/Licenses.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,8 @@ namespace Grav\Common\GPM;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
+use RocketTheme\Toolbox\File\FileInterface;
+use function is_string;
/**
* Class Licenses
@@ -19,23 +21,16 @@ use Grav\Common\Grav;
*/
class Licenses
{
-
- /**
- * Regex to validate the format of a License
- *
- * @var string
- */
+ /** @var string Regex to validate the format of a License */
protected static $regex = '^(?:[A-F0-9]{8}-){3}(?:[A-F0-9]{8}){1}$';
-
+ /** @var FileInterface */
protected static $file;
-
/**
* Returns the license for a Premium package
*
* @param string $slug
* @param string $license
- *
* @return bool
*/
public static function set($slug, $license)
@@ -48,7 +43,7 @@ class Licenses
return false;
}
- if (!\is_string($license)) {
+ if (!is_string($license)) {
if (isset($data['licenses'][$slug])) {
unset($data['licenses'][$slug]);
} else {
@@ -67,21 +62,21 @@ class Licenses
/**
* Returns the license for a Premium package
*
- * @param string $slug
- *
- * @return array|string
+ * @param string|null $slug
+ * @return string[]|string
*/
public static function get($slug = null)
{
$licenses = self::getLicenseFile();
$data = (array)$licenses->content();
$licenses->free();
- $slug = strtolower($slug);
- if (!$slug) {
+ if (null === $slug) {
return $data['licenses'] ?? [];
}
+ $slug = strtolower($slug);
+
return $data['licenses'][$slug] ?? '';
}
@@ -89,8 +84,7 @@ class Licenses
/**
* Validates the License format
*
- * @param string $license
- *
+ * @param string|null $license
* @return bool
*/
public static function validate($license = null)
@@ -99,16 +93,15 @@ class Licenses
return false;
}
- return preg_match('#' . self::$regex. '#', $license);
+ return (bool)preg_match('#' . self::$regex. '#', $license);
}
/**
* Get the License File object
*
- * @return \RocketTheme\Toolbox\File\FileInterface
+ * @return FileInterface
*/
public static function getLicenseFile()
-
{
if (!isset(self::$file)) {
$path = Grav::instance()['locator']->findResource('user-data://') . '/licenses.yaml';
diff --git a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php
index be565c9c..d1f3e467 100644
--- a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php
+++ b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,8 +11,17 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
+/**
+ * Class AbstractPackageCollection
+ * @package Grav\Common\GPM\Local
+ */
abstract class AbstractPackageCollection extends BaseCollection
{
+ /**
+ * AbstractPackageCollection constructor.
+ *
+ * @param array $items
+ */
public function __construct($items)
{
parent::__construct();
diff --git a/system/src/Grav/Common/GPM/Local/Package.php b/system/src/Grav/Common/GPM/Local/Package.php
index 88241a44..ffe2c63c 100644
--- a/system/src/Grav/Common/GPM/Local/Package.php
+++ b/system/src/Grav/Common/GPM/Local/Package.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,12 +11,22 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\Data\Data;
use Grav\Common\GPM\Common\Package as BasePackage;
-use Grav\Framework\Parsedown\Parsedown;
+use Parsedown;
+/**
+ * Class Package
+ * @package Grav\Common\GPM\Local
+ */
class Package extends BasePackage
{
+ /** @var array */
protected $settings;
+ /**
+ * Package constructor.
+ * @param Data $package
+ * @param string|null $package_type
+ */
public function __construct(Data $package, $package_type = null)
{
$data = new Data($package->blueprints()->toArray());
@@ -32,10 +42,10 @@ class Package extends BasePackage
}
/**
- * @return mixed
+ * @return bool
*/
public function isEnabled()
{
- return $this->settings['enabled'];
+ return (bool)$this->settings['enabled'];
}
}
diff --git a/system/src/Grav/Common/GPM/Local/Packages.php b/system/src/Grav/Common/GPM/Local/Packages.php
index 14a0b0b4..0290296c 100644
--- a/system/src/Grav/Common/GPM/Local/Packages.php
+++ b/system/src/Grav/Common/GPM/Local/Packages.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,10 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\GPM\Common\CachedCollection;
+/**
+ * Class Packages
+ * @package Grav\Common\GPM\Local
+ */
class Packages extends CachedCollection
{
public function __construct()
diff --git a/system/src/Grav/Common/GPM/Local/Plugins.php b/system/src/Grav/Common/GPM/Local/Plugins.php
index 19adfb88..8a6574b5 100644
--- a/system/src/Grav/Common/GPM/Local/Plugins.php
+++ b/system/src/Grav/Common/GPM/Local/Plugins.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,11 +11,13 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\Grav;
+/**
+ * Class Plugins
+ * @package Grav\Common\GPM\Local
+ */
class Plugins extends AbstractPackageCollection
{
- /**
- * @var string
- */
+ /** @var string */
protected $type = 'plugins';
/**
diff --git a/system/src/Grav/Common/GPM/Local/Themes.php b/system/src/Grav/Common/GPM/Local/Themes.php
index 8607e6b7..73253ccd 100644
--- a/system/src/Grav/Common/GPM/Local/Themes.php
+++ b/system/src/Grav/Common/GPM/Local/Themes.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,11 +11,13 @@ namespace Grav\Common\GPM\Local;
use Grav\Common\Grav;
+/**
+ * Class Themes
+ * @package Grav\Common\GPM\Local
+ */
class Themes extends AbstractPackageCollection
{
- /**
- * @var string
- */
+ /** @var string */
protected $type = 'themes';
/**
@@ -23,6 +25,9 @@ class Themes extends AbstractPackageCollection
*/
public function __construct()
{
- parent::__construct(Grav::instance()['themes']->all());
+ /** @var \Grav\Common\Themes $themes */
+ $themes = Grav::instance()['themes'];
+
+ parent::__construct($themes->all());
}
}
diff --git a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php
index 429c5b1d..9e68d666 100644
--- a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php
+++ b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,37 +13,36 @@ use Grav\Common\Grav;
use Grav\Common\GPM\Common\AbstractPackageCollection as BaseCollection;
use Grav\Common\GPM\Response;
use \Doctrine\Common\Cache\FilesystemCache;
+use RuntimeException;
+/**
+ * Class AbstractPackageCollection
+ * @package Grav\Common\GPM\Remote
+ */
class AbstractPackageCollection extends BaseCollection
{
- /**
- * The cached data previously fetched
- * @var string
- */
+ /** @var string The cached data previously fetched */
protected $raw;
-
- /**
- * The lifetime to store the entry in seconds
- * @var int
- */
- private $lifetime = 86400;
-
+ /** @var string */
protected $repository;
-
+ /** @var FilesystemCache */
protected $cache;
+ /** @var int The lifetime to store the entry in seconds */
+ private $lifetime = 86400;
+
/**
* AbstractPackageCollection constructor.
*
- * @param null $repository
+ * @param string|null $repository
* @param bool $refresh
- * @param null $callback
+ * @param callable|null $callback
*/
public function __construct($repository = null, $refresh = false, $callback = null)
{
parent::__construct();
if ($repository === null) {
- throw new \RuntimeException('A repository is required to indicate the origin of the remote collection');
+ throw new RuntimeException('A repository is required to indicate the origin of the remote collection');
}
$channel = Grav::instance()['config']->get('system.gpm.releases', 'stable');
@@ -55,7 +54,7 @@ class AbstractPackageCollection extends BaseCollection
$this->fetch($refresh, $callback);
foreach (json_decode($this->raw, true) as $slug => $data) {
- // Temporarily fix for using multisites
+ // Temporarily fix for using multi-sites
if (isset($data['install_path'])) {
$path = preg_replace('~^user/~i', 'user://', $data['install_path']);
$data['install_path'] = Grav::instance()['locator']->findResource($path, false, true);
@@ -64,6 +63,11 @@ class AbstractPackageCollection extends BaseCollection
}
}
+ /**
+ * @param bool $refresh
+ * @param callable|null $callback
+ * @return string
+ */
public function fetch($refresh = false, $callback = null)
{
if (!$this->raw || $refresh) {
diff --git a/system/src/Grav/Common/GPM/Remote/GravCore.php b/system/src/Grav/Common/GPM/Remote/GravCore.php
index 1d30120d..f93209ed 100644
--- a/system/src/Grav/Common/GPM/Remote/GravCore.php
+++ b/system/src/Grav/Common/GPM/Remote/GravCore.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,20 +11,30 @@ namespace Grav\Common\GPM\Remote;
use Grav\Common\Grav;
use \Doctrine\Common\Cache\FilesystemCache;
+use InvalidArgumentException;
+/**
+ * Class GravCore
+ * @package Grav\Common\GPM\Remote
+ */
class GravCore extends AbstractPackageCollection
{
+ /** @var string */
protected $repository = 'https://getgrav.org/downloads/grav.json';
- private $data;
+ /** @var array */
+ private $data;
+ /** @var string */
private $version;
+ /** @var string */
private $date;
+ /** @var string|null */
private $min_php;
/**
* @param bool $refresh
- * @param null $callback
- * @throws \InvalidArgumentException
+ * @param callable|null $callback
+ * @throws InvalidArgumentException
*/
public function __construct($refresh = false, $callback = null)
{
@@ -61,8 +71,7 @@ class GravCore extends AbstractPackageCollection
/**
* Returns the changelog list for each version of Grav
*
- * @param string $diff the version number to start the diff from
- *
+ * @param string|null $diff the version number to start the diff from
* @return array changelog list for each version
*/
public function getChangelog($diff = null)
@@ -118,14 +127,15 @@ class GravCore extends AbstractPackageCollection
/**
* Returns the minimum PHP version
*
- * @return null|string
+ * @return string
*/
public function getMinPHPVersion()
{
// If non min set, assume current PHP version
if (null === $this->min_php) {
- $this->min_php = phpversion();
+ $this->min_php = PHP_VERSION;
}
+
return $this->min_php;
}
diff --git a/system/src/Grav/Common/GPM/Remote/Package.php b/system/src/Grav/Common/GPM/Remote/Package.php
index 196e92bd..c37d6a00 100644
--- a/system/src/Grav/Common/GPM/Remote/Package.php
+++ b/system/src/Grav/Common/GPM/Remote/Package.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,16 +12,54 @@ namespace Grav\Common\GPM\Remote;
use Grav\Common\Data\Data;
use Grav\Common\GPM\Common\Package as BasePackage;
+/**
+ * Class Package
+ * @package Grav\Common\GPM\Remote
+ */
class Package extends BasePackage implements \JsonSerializable
{
+ /**
+ * Package constructor.
+ * @param array $package
+ * @param string|null $package_type
+ */
public function __construct($package, $package_type = null)
{
$data = new Data($package);
parent::__construct($data, $package_type);
}
+ /**
+ * @return array
+ */
public function jsonSerialize()
{
- return $this->data;
+ return $this->data->toArray();
+ }
+
+ /**
+ * Returns the changelog list for each version of a package
+ *
+ * @param string|null $diff the version number to start the diff from
+ * @return array changelog list for each version
+ */
+ public function getChangelog($diff = null)
+ {
+ if (!$diff) {
+ return $this->data['changelog'];
+ }
+
+ $diffLog = [];
+ foreach ((array)$this->data['changelog'] as $version => $changelog) {
+ preg_match("/[\w\-.]+/", $version, $cleanVersion);
+
+ if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) {
+ continue;
+ }
+
+ $diffLog[$version] = $changelog;
+ }
+
+ return $diffLog;
}
}
diff --git a/system/src/Grav/Common/GPM/Remote/Packages.php b/system/src/Grav/Common/GPM/Remote/Packages.php
index 46bc31ff..f55c12dc 100644
--- a/system/src/Grav/Common/GPM/Remote/Packages.php
+++ b/system/src/Grav/Common/GPM/Remote/Packages.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,8 +11,17 @@ namespace Grav\Common\GPM\Remote;
use Grav\Common\GPM\Common\CachedCollection;
+/**
+ * Class Packages
+ * @package Grav\Common\GPM\Remote
+ */
class Packages extends CachedCollection
{
+ /**
+ * Packages constructor.
+ * @param bool $refresh
+ * @param callable|null $callback
+ */
public function __construct($refresh = false, $callback = null)
{
$items = [
diff --git a/system/src/Grav/Common/GPM/Remote/Plugins.php b/system/src/Grav/Common/GPM/Remote/Plugins.php
index 1d905e37..a134a104 100644
--- a/system/src/Grav/Common/GPM/Remote/Plugins.php
+++ b/system/src/Grav/Common/GPM/Remote/Plugins.php
@@ -3,25 +3,27 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Remote;
+/**
+ * Class Plugins
+ * @package Grav\Common\GPM\Remote
+ */
class Plugins extends AbstractPackageCollection
{
- /**
- * @var string
- */
+ /** @var string */
protected $type = 'plugins';
-
+ /** @var string */
protected $repository = 'https://getgrav.org/downloads/plugins.json';
/**
* Local Plugins Constructor
* @param bool $refresh
- * @param callable $callback Either a function or callback in array notation
+ * @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
{
diff --git a/system/src/Grav/Common/GPM/Remote/Themes.php b/system/src/Grav/Common/GPM/Remote/Themes.php
index 8024b40c..160ac97a 100644
--- a/system/src/Grav/Common/GPM/Remote/Themes.php
+++ b/system/src/Grav/Common/GPM/Remote/Themes.php
@@ -3,25 +3,27 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM\Remote;
+/**
+ * Class Themes
+ * @package Grav\Common\GPM\Remote
+ */
class Themes extends AbstractPackageCollection
{
- /**
- * @var string
- */
+ /** @var string */
protected $type = 'themes';
-
+ /** @var string */
protected $repository = 'https://getgrav.org/downloads/themes.json';
/**
* Local Themes Constructor
* @param bool $refresh
- * @param callable $callback Either a function or callback in array notation
+ * @param callable|null $callback Either a function or callback in array notation
*/
public function __construct($refresh = false, $callback = null)
{
diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php
index 221ecf07..47cef7c0 100644
--- a/system/src/Grav/Common/GPM/Response.php
+++ b/system/src/Grav/Common/GPM/Response.php
@@ -3,202 +3,107 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
+use Exception;
use Grav\Common\Utils;
use Grav\Common\Grav;
+use Symfony\Component\HttpClient\CurlHttpClient;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\HttpClient;
+use Symfony\Component\HttpClient\HttpOptions;
+use Symfony\Component\HttpClient\NativeHttpClient;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use function call_user_func;
+use function defined;
+use function function_exists;
+/**
+ * Class Response
+ * @package Grav\Common\GPM
+ */
class Response
{
- /**
- * The callback for the progress
- *
- * @var callable Either a function or callback in array notation
- */
+ /** @var callable The callback for the progress, either a function or callback in array notation */
public static $callback = null;
-
- /**
- * Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method
- *
- * @var string
- */
- private static $method = 'auto';
-
- /**
- * Default parameters for `curl` and `fopen`
- *
- * @var array
- */
- private static $defaults = [
-
- 'curl' => [
- CURLOPT_REFERER => 'Grav GPM',
- CURLOPT_USERAGENT => 'Grav GPM',
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_FAILONERROR => true,
- CURLOPT_TIMEOUT => 15,
- CURLOPT_HEADER => false,
- //CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
- /**
- * Example of callback parameters from within your own class
- */
- //CURLOPT_NOPROGRESS => false,
- //CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
- ],
- 'fopen' => [
- 'method' => 'GET',
- 'user_agent' => 'Grav GPM',
- 'max_redirects' => 5,
- 'follow_location' => 1,
- 'timeout' => 15,
- /* // this is set in the constructor since it's a setting
- 'ssl' => [
- 'verify_peer' => true,
- 'verify_peer_name' => true,
- ],
- */
- /**
- * Example of callback parameters from within your own class
- */
- //'notification' => [$this, 'progress']
- ]
+ /** @var string[] */
+ private static $headers = [
+ 'User-Agent' => 'Grav CMS'
];
- /**
- * Sets the preferred method to use for making HTTP calls.
- *
- * @param string $method Default is `auto`
- *
- * @return Response
- */
- public static function setMethod($method = 'auto')
- {
- if (!\in_array($method, ['auto', 'curl', 'fopen'], true)) {
- $method = 'auto';
- }
-
- self::$method = $method;
-
- return new self();
- }
-
/**
* Makes a request to the URL by using the preferred method
*
- * @param string $uri URL to call
- * @param array $options An array of parameters for both `curl` and `fopen`
- * @param callable $callback Either a function or callback in array notation
- *
+ * @param string $uri URL to call
+ * @param array $overrides An array of parameters for both `curl` and `fopen`
+ * @param callable|null $callback Either a function or callback in array notation
* @return string The response of the request
+ * @throws TransportExceptionInterface
*/
- public static function get($uri = '', $options = [], $callback = null)
+ public static function get($uri = '', $overrides = [], $callback = null)
{
- if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
- throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
+ if (empty($uri)) {
+ throw new TransportException('missing URI');
}
// check if this function is available, if so use it to stop any timeouts
try {
- if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) {
- set_time_limit(0);
+ if (Utils::functionExists('set_time_limit')) {
+ @set_time_limit(0);
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
}
$config = Grav::instance()['config'];
- $overrides = [];
+ $referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
+ $options = new HttpOptions();
- // Override CA Bundle
- $caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
- if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
- $overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile;
- $overrides['fopen']['ssl']['capath'] = $caPathOrFile;
- } else {
- $overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile;
- $overrides['fopen']['ssl']['cafile'] = $caPathOrFile;
- }
+ // Set default Headers
+ $options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers));
- // SSL Verify Peer and Proxy Setting
- $settings = [
- 'method' => $config->get('system.gpm.method', self::$method),
- 'verify_peer' => $config->get('system.gpm.verify_peer', true),
- // `system.proxy_url` is for fallback
- // introduced with 1.1.0-beta.1 probably safe to remove at some point
- 'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
- ];
+ // Disable verify Peer if required
+ $verify_peer = $config->get('system.gpm.verify_peer', true);
+ if ($verify_peer !== true) {
+ $options->verifyPeer($verify_peer);
+ }
- if (!$settings['verify_peer']) {
- $overrides = array_replace_recursive([], $overrides, [
- 'curl' => [
- CURLOPT_SSL_VERIFYPEER => $settings['verify_peer']
- ],
- 'fopen' => [
- 'ssl' => [
- 'verify_peer' => $settings['verify_peer'],
- 'verify_peer_name' => $settings['verify_peer'],
- ]
- ]
- ]);
+ // Set proxy url if provided
+ $proxy_url = $config->get('system.gpm.proxy_url', false);
+ if ($proxy_url) {
+ $options->setProxy($proxy_url);
}
- // Proxy Setting
- if ($settings['proxy_url']) {
- $proxy = parse_url($settings['proxy_url']);
- $fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
+ // Use callback if provided
+ if ($callback) {
+ self::$callback = $callback;
+ $options->setOnProgress([Response::class, 'progress']);
+ }
- $overrides = array_replace_recursive([], $overrides, [
- 'curl' => [
- CURLOPT_PROXY => $proxy['host'],
- CURLOPT_PROXYTYPE => 'HTTP'
- ],
- 'fopen' => [
- 'proxy' => $fopen_proxy,
- 'request_fulluri' => true
- ]
- ]);
+ $preferred_method = $config->get('system.gpm.method', 'auto');
- if (isset($proxy['port'])) {
- $overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
- }
+ $settings = array_merge_recursive($options->toArray(), $overrides);
- if (isset($proxy['user'], $proxy['pass'])) {
- $fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
- $overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
- $overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
- }
+ switch ($preferred_method) {
+ case 'curl':
+ $client = new CurlHttpClient($settings);
+ break;
+ case 'fopen':
+ case 'native':
+ $client = new NativeHttpClient($settings);
+ break;
+ default:
+ $client = HttpClient::create($settings);
}
- $options = array_replace_recursive(self::$defaults, $options, $overrides);
- $method = 'get' . ucfirst(strtolower($settings['method']));
+ $response = $client->request('GET', $uri);
- self::$callback = $callback;
- return static::$method($uri, $options, $callback);
+ return $response->getContent();
}
- /**
- * Checks if cURL is available
- *
- * @return bool
- */
- public static function isCurlAvailable()
- {
- return function_exists('curl_version');
- }
-
- /**
- * Checks if the remote fopen request is enabled in PHP
- *
- * @return bool
- */
- public static function isFopenAvailable()
- {
- return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
- }
/**
* Is this a remote file or not
@@ -214,221 +119,25 @@ class Response
/**
* Progress normalized for cURL and Fopen
* Accepts a variable length of arguments passed in by stream method
- */
- public static function progress()
- {
- static $filesize = null;
-
- $args = func_get_args();
- $isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) === 'curl';
-
- $notification_code = !$isCurlResource ? $args[0] : false;
- $bytes_transferred = $isCurlResource ? $args[2] : $args[4];
-
- if ($isCurlResource) {
- $filesize = $args[1];
- } elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
- $filesize = $args[5];
- }
-
- if ($bytes_transferred > 0) {
- if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) {
-
- $progress = [
- 'code' => $notification_code,
- 'filesize' => $filesize,
- 'transferred' => $bytes_transferred,
- 'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1)
- ];
-
- if (self::$callback !== null) {
- call_user_func(self::$callback, $progress);
- }
- }
- }
- }
-
- /**
- * Automatically picks the preferred method
- *
- * @return string The response of the request
- */
- private static function getAuto()
- {
- if (!ini_get('open_basedir') && self::isFopenAvailable()) {
- return self::getFopen(func_get_args());
- }
-
- if (self::isCurlAvailable()) {
- return self::getCurl(func_get_args());
- }
-
- return null;
- }
-
- /**
- * Starts a HTTP request via fopen
- *
- * @return string The response of the request
- */
- private static function getFopen()
- {
- if (\count($args = func_get_args()) === 1) {
- $args = $args[0];
- }
-
- $uri = $args[0];
- $options = $args[1] ?? [];
- $callback = $args[2] ?? null;
-
- if ($callback) {
- $options['fopen']['notification'] = ['self', 'progress'];
- }
-
- if (isset($options['fopen']['ssl'])) {
- $ssl = $options['fopen']['ssl'];
- unset($options['fopen']['ssl']);
-
- $stream = stream_context_create([
- 'http' => $options['fopen'],
- 'ssl' => $ssl
- ], $options['fopen']);
- } else {
- $stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
- }
-
-
- $content = @file_get_contents($uri, false, $stream);
-
- if ($content === false) {
- $code = null;
- // Function file_get_contents() creates local variable $http_response_header, check it
- if (isset($http_response_header)) {
- $code = explode(' ', $http_response_header[0] ?? '')[1] ?? null;
- }
-
- switch ($code) {
- case '404':
- throw new \RuntimeException('Page not found');
- case '401':
- throw new \RuntimeException('Invalid LICENSE');
- default:
- throw new \RuntimeException("Error while trying to download (code: {$code}): {$uri}\n");
- }
- }
-
- return $content;
- }
-
- /**
- * Starts a HTTP request via cURL
*
- * @return string The response of the request
+ * @return void
*/
- private static function getCurl()
+ public static function progress(int $bytes_transferred, int $filesize, array $info)
{
- $args = func_get_args();
- $args = count($args) > 1 ? $args : array_shift($args);
-
- $uri = $args[0];
- $options = $args[1] ?? [];
- $callback = $args[2] ?? null;
-
- $ch = curl_init($uri);
-
- $response = static::curlExecFollow($ch, $options, $callback);
- $errno = curl_errno($ch);
-
- if ($errno) {
- $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $error_message = curl_strerror($errno) . "\n" . curl_error($ch);
-
- switch ($code) {
- case '404':
- throw new \RuntimeException('Page not found');
- case '401':
- throw new \RuntimeException('Invalid LICENSE');
- default:
- throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
- }
- }
- curl_close($ch);
-
- return $response;
- }
-
- /**
- * @param resource $ch
- * @param array $options
- * @param bool $callback
- *
- * @return bool|mixed
- */
- private static function curlExecFollow($ch, $options, $callback)
- {
- if ($callback) {
- curl_setopt_array(
- $ch,
- [
- CURLOPT_NOPROGRESS => false,
- CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
- ]
- );
- }
-
- // no open_basedir set, we can proceed normally
- if (!ini_get('open_basedir')) {
- curl_setopt_array($ch, $options['curl']);
- return curl_exec($ch);
- }
-
- $max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5;
- $options['curl'][CURLOPT_FOLLOWLOCATION] = false;
-
- // open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
- curl_setopt_array($ch, $options['curl']);
- if ($max_redirects <= 0) {
- return curl_exec($ch);
- }
-
- $uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
- $rch = curl_copy_handle($ch);
-
- curl_setopt($rch, CURLOPT_HEADER, true);
- curl_setopt($rch, CURLOPT_NOBODY, true);
- curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
- curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
-
- do {
- curl_setopt($rch, CURLOPT_URL, $uri);
- $header = curl_exec($rch);
-
- if (curl_errno($rch)) {
- $code = 0;
- } else {
- $code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
- if ($code === 301 || $code === 302 || $code === 303) {
- preg_match('/(?:^|\n)Location:(.*?)\n/i', $header, $matches);
- $uri = trim(array_pop($matches));
- } else {
- $code = 0;
- }
- }
- } while ($code && --$max_redirects);
+ if ($bytes_transferred > 0) {
+ $percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize);
- curl_close($rch);
+ $progress = [
+ 'code' => $info['http_code'],
+ 'filesize' => $filesize,
+ 'transferred' => $bytes_transferred,
+ 'percent' => $percent < 100 ? $percent : 100
+ ];
- if (!$max_redirects) {
- if ($max_redirects === null) {
- trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
+ if (self::$callback !== null) {
+ call_user_func(self::$callback, $progress);
}
-
- return false;
}
-
- curl_setopt($ch, CURLOPT_URL, $uri);
-
- return curl_exec($ch);
}
}
diff --git a/system/src/Grav/Common/GPM/Upgrader.php b/system/src/Grav/Common/GPM/Upgrader.php
index 5d9406d5..dfa6eb13 100644
--- a/system/src/Grav/Common/GPM/Upgrader.php
+++ b/system/src/Grav/Common/GPM/Upgrader.php
@@ -3,13 +3,14 @@
/**
* @package Grav\Common\GPM
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
use Grav\Common\GPM\Remote\GravCore;
+use InvalidArgumentException;
/**
* Class Upgrader
@@ -18,21 +19,18 @@ use Grav\Common\GPM\Remote\GravCore;
*/
class Upgrader
{
- /**
- * Remote details about latest Grav version
- *
- * @var GravCore
- */
+ /** @var GravCore Remote details about latest Grav version */
private $remote;
+ /** @var string|null */
private $min_php;
/**
* Creates a new GPM instance with Local and Remote packages available
*
* @param boolean $refresh Applies to Remote Packages only and forces a refetch of data
- * @param callable $callback Either a function or callback in array notation
- * @throws \InvalidArgumentException
+ * @param callable|null $callback Either a function or callback in array notation
+ * @throws InvalidArgumentException
*/
public function __construct($refresh = false, $callback = null)
{
@@ -82,8 +80,7 @@ class Upgrader
/**
* Returns the changelog list for each version of Grav
*
- * @param string $diff the version number to start the diff from
- *
+ * @param string|null $diff the version number to start the diff from
* @return array return the changelog list for each version
*/
public function getChangelog($diff = null)
@@ -98,8 +95,7 @@ class Upgrader
*/
public function meetsRequirements()
{
- $current_php_version = phpversion();
- if (version_compare($current_php_version, $this->minPHPVersion(), '<')) {
+ if (version_compare(PHP_VERSION, $this->minPHPVersion(), '<')) {
return false;
}
@@ -109,20 +105,21 @@ class Upgrader
/**
* Get minimum PHP version from remote
*
- * @return null
+ * @return string
*/
public function minPHPVersion()
{
if (null === $this->min_php) {
$this->min_php = $this->remote->getMinPHPVersion();
}
+
return $this->min_php;
}
/**
* Checks if the currently installed Grav is upgradable to a newer version
*
- * @return boolean True if it's upgradable, False otherwise.
+ * @return bool True if it's upgradable, False otherwise.
*/
public function isUpgradable()
{
@@ -132,9 +129,8 @@ class Upgrader
/**
* Checks if Grav is currently symbolically linked
*
- * @return boolean True if Grav is symlinked, False otherwise.
+ * @return bool True if Grav is symlinked, False otherwise.
*/
-
public function isSymlink()
{
return $this->remote->isSymlink();
diff --git a/system/src/Grav/Common/Getters.php b/system/src/Grav/Common/Getters.php
index e69116e1..8f3a73a2 100644
--- a/system/src/Grav/Common/Getters.php
+++ b/system/src/Grav/Common/Getters.php
@@ -3,25 +3,29 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
-abstract class Getters implements \ArrayAccess, \Countable
+use ArrayAccess;
+use Countable;
+use function count;
+
+/**
+ * Class Getters
+ * @package Grav\Common
+ */
+abstract class Getters implements ArrayAccess, Countable
{
- /**
- * Define variable used in getters.
- *
- * @var string
- */
+ /** @var string Define variable used in getters. */
protected $gettersVariable = null;
/**
* Magic setter method
*
- * @param mixed $offset Medium name value
+ * @param int|string $offset Medium name value
* @param mixed $value Medium value
*/
public function __set($offset, $value)
@@ -32,8 +36,7 @@ abstract class Getters implements \ArrayAccess, \Countable
/**
* Magic getter method
*
- * @param mixed $offset Medium name value
- *
+ * @param int|string $offset Medium name value
* @return mixed Medium value
*/
public function __get($offset)
@@ -44,8 +47,7 @@ abstract class Getters implements \ArrayAccess, \Countable
/**
* Magic method to determine if the attribute is set
*
- * @param mixed $offset Medium name value
- *
+ * @param int|string $offset Medium name value
* @return boolean True if the value is set
*/
public function __isset($offset)
@@ -56,7 +58,7 @@ abstract class Getters implements \ArrayAccess, \Countable
/**
* Magic method to unset the attribute
*
- * @param mixed $offset The name value to unset
+ * @param int|string $offset The name value to unset
*/
public function __unset($offset)
{
@@ -64,8 +66,7 @@ abstract class Getters implements \ArrayAccess, \Countable
}
/**
- * @param mixed $offset
- *
+ * @param int|string $offset
* @return bool
*/
public function offsetExists($offset)
@@ -80,8 +81,7 @@ abstract class Getters implements \ArrayAccess, \Countable
}
/**
- * @param mixed $offset
- *
+ * @param int|string $offset
* @return mixed
*/
public function offsetGet($offset)
@@ -96,7 +96,7 @@ abstract class Getters implements \ArrayAccess, \Countable
}
/**
- * @param mixed $offset
+ * @param int|string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
@@ -110,7 +110,7 @@ abstract class Getters implements \ArrayAccess, \Countable
}
/**
- * @param mixed $offset
+ * @param int|string $offset
*/
public function offsetUnset($offset)
{
@@ -129,10 +129,10 @@ abstract class Getters implements \ArrayAccess, \Countable
{
if ($this->gettersVariable) {
$var = $this->gettersVariable;
- return \count($this->{$var});
+ return count($this->{$var});
}
- return \count($this->toArray());
+ return count($this->toArray());
}
/**
diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php
index c7287dbd..f5919613 100644
--- a/system/src/Grav/Common/Grav.php
+++ b/system/src/Grav/Common/Grav.php
@@ -3,25 +3,24 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use Composer\Autoload\ClassLoader;
use Grav\Common\Config\Config;
use Grav\Common\Config\Setup;
+use Grav\Common\Helpers\Exif;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
+use Grav\Common\Page\Pages;
use Grav\Common\Processors\AssetsProcessor;
use Grav\Common\Processors\BackupsProcessor;
-use Grav\Common\Processors\ConfigurationProcessor;
use Grav\Common\Processors\DebuggerAssetsProcessor;
-use Grav\Common\Processors\DebuggerProcessor;
-use Grav\Common\Processors\ErrorsProcessor;
use Grav\Common\Processors\InitializeProcessor;
-use Grav\Common\Processors\LoggerProcessor;
use Grav\Common\Processors\PagesProcessor;
use Grav\Common\Processors\PluginsProcessor;
use Grav\Common\Processors\RenderProcessor;
@@ -30,13 +29,40 @@ use Grav\Common\Processors\SchedulerProcessor;
use Grav\Common\Processors\TasksProcessor;
use Grav\Common\Processors\ThemesProcessor;
use Grav\Common\Processors\TwigProcessor;
+use Grav\Common\Scheduler\Scheduler;
+use Grav\Common\Service\AccountsServiceProvider;
+use Grav\Common\Service\AssetsServiceProvider;
+use Grav\Common\Service\BackupsServiceProvider;
+use Grav\Common\Service\ConfigServiceProvider;
+use Grav\Common\Service\ErrorServiceProvider;
+use Grav\Common\Service\FilesystemServiceProvider;
+use Grav\Common\Service\FlexServiceProvider;
+use Grav\Common\Service\InflectorServiceProvider;
+use Grav\Common\Service\LoggerServiceProvider;
+use Grav\Common\Service\OutputServiceProvider;
+use Grav\Common\Service\PagesServiceProvider;
+use Grav\Common\Service\RequestServiceProvider;
+use Grav\Common\Service\SessionServiceProvider;
+use Grav\Common\Service\StreamsServiceProvider;
+use Grav\Common\Service\TaskServiceProvider;
+use Grav\Common\Twig\Twig;
use Grav\Framework\DI\Container;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\RequestHandler;
+use Grav\Framework\Session\Messages;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
-use RocketTheme\Toolbox\Event\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use function array_key_exists;
+use function call_user_func_array;
+use function function_exists;
+use function get_class;
+use function in_array;
+use function is_callable;
+use function is_int;
+use function strlen;
/**
* Grav container is the heart of Grav.
@@ -45,14 +71,10 @@ use RocketTheme\Toolbox\Event\EventDispatcher;
*/
class Grav extends Container
{
- /**
- * @var string Processed output for the page.
- */
+ /** @var string Processed output for the page. */
public $output;
- /**
- * @var static The singleton instance
- */
+ /** @var static The singleton instance */
protected static $instance;
/**
@@ -60,40 +82,37 @@ class Grav extends Container
* to the dependency injection container.
*/
protected static $diMap = [
- 'Grav\Common\Service\AccountsServiceProvider',
- 'Grav\Common\Service\AssetsServiceProvider',
- 'Grav\Common\Service\BackupsServiceProvider',
- 'Grav\Common\Service\ConfigServiceProvider',
- 'Grav\Common\Service\ErrorServiceProvider',
- 'Grav\Common\Service\FilesystemServiceProvider',
- 'Grav\Common\Service\InflectorServiceProvider',
- 'Grav\Common\Service\LoggerServiceProvider',
- 'Grav\Common\Service\OutputServiceProvider',
- 'Grav\Common\Service\PagesServiceProvider',
- 'Grav\Common\Service\RequestServiceProvider',
- 'Grav\Common\Service\SessionServiceProvider',
- 'Grav\Common\Service\StreamsServiceProvider',
- 'Grav\Common\Service\TaskServiceProvider',
- 'browser' => 'Grav\Common\Browser',
- 'cache' => 'Grav\Common\Cache',
- 'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
- 'exif' => 'Grav\Common\Helpers\Exif',
- 'plugins' => 'Grav\Common\Plugins',
- 'scheduler' => 'Grav\Common\Scheduler\Scheduler',
- 'taxonomy' => 'Grav\Common\Taxonomy',
- 'themes' => 'Grav\Common\Themes',
- 'twig' => 'Grav\Common\Twig\Twig',
- 'uri' => 'Grav\Common\Uri',
+ AccountsServiceProvider::class,
+ AssetsServiceProvider::class,
+ BackupsServiceProvider::class,
+ ConfigServiceProvider::class,
+ ErrorServiceProvider::class,
+ FilesystemServiceProvider::class,
+ FlexServiceProvider::class,
+ InflectorServiceProvider::class,
+ LoggerServiceProvider::class,
+ OutputServiceProvider::class,
+ PagesServiceProvider::class,
+ RequestServiceProvider::class,
+ SessionServiceProvider::class,
+ StreamsServiceProvider::class,
+ TaskServiceProvider::class,
+ 'browser' => Browser::class,
+ 'cache' => Cache::class,
+ 'events' => EventDispatcher::class,
+ 'exif' => Exif::class,
+ 'plugins' => Plugins::class,
+ 'scheduler' => Scheduler::class,
+ 'taxonomy' => Taxonomy::class,
+ 'themes' => Themes::class,
+ 'twig' => Twig::class,
+ 'uri' => Uri::class,
];
/**
* @var array All middleware processors that are processed in $this->process()
*/
protected $middleware = [
- 'configurationProcessor',
- 'loggerProcessor',
- 'errorsProcessor',
- 'debuggerProcessor',
'initializeProcessor',
'pluginsProcessor',
'themesProcessor',
@@ -108,14 +127,18 @@ class Grav extends Container
'renderProcessor',
];
+ /** @var array */
protected $initialized = [];
/**
* Reset the Grav instance.
+ *
+ * @return void
*/
- public static function resetInstance()
+ public static function resetInstance(): void
{
if (self::$instance) {
+ // @phpstan-ignore-next-line
self::$instance = null;
}
}
@@ -124,13 +147,19 @@ class Grav extends Container
* Return the Grav instance. Create it if it's not already instanced
*
* @param array $values
- *
* @return Grav
*/
public static function instance(array $values = [])
{
- if (!self::$instance) {
+ if (null === self::$instance) {
self::$instance = static::load($values);
+
+ /** @var ClassLoader|null $loader */
+ $loader = self::$instance['loader'] ?? null;
+ if ($loader) {
+ // Load fix for Deferred Twig Extension
+ $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
+ }
} elseif ($values) {
$instance = self::$instance;
foreach ($values as $key => $value) {
@@ -142,9 +171,25 @@ class Grav extends Container
}
/**
- * Setup Grav instance using specific environment.
+ * Get Grav version.
*
- * Initializes Grav streams by
+ * @return string
+ */
+ public function getVersion(): string
+ {
+ return GRAV_VERSION;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isSetup(): bool
+ {
+ return isset($this->initialized['setup']);
+ }
+
+ /**
+ * Setup Grav instance using specific environment.
*
* @param string|null $environment
* @return $this
@@ -157,15 +202,14 @@ class Grav extends Container
$this->initialized['setup'] = true;
- $this->measureTime('_setup', 'Site Setup', function () use ($environment) {
- // Force environment if passed to the method.
- if ($environment) {
- Setup::$environment = $environment;
- }
+ // Force environment if passed to the method.
+ if ($environment) {
+ Setup::$environment = $environment;
+ }
- $this['setup'];
- $this['streams'];
- });
+ // Initialize setup and streams.
+ $this['setup'];
+ $this['streams'];
return $this;
}
@@ -176,14 +220,14 @@ class Grav extends Container
* Call after `$grav->setup($environment)`
*
* - Load configuration
+ * - Initialize logger
* - Disable debugger
* - Set timezone, locale
- * - Load plugins
- * - Set Users type to be used in the site
+ * - Load plugins (call PluginsLoadedEvent)
+ * - Set Pages and 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()
@@ -195,8 +239,10 @@ class Grav extends Container
/**
* Process a request
+ *
+ * @return void
*/
- public function process()
+ public function process(): void
{
if (isset($this->initialized['process'])) {
return;
@@ -209,18 +255,6 @@ class Grav extends Container
$container = new Container(
[
- 'configurationProcessor' => function () {
- return new ConfigurationProcessor($this);
- },
- 'loggerProcessor' => function () {
- return new LoggerProcessor($this);
- },
- 'errorsProcessor' => function () {
- return new ErrorsProcessor($this);
- },
- 'debuggerProcessor' => function () {
- return new DebuggerProcessor($this);
- },
'initializeProcessor' => function () {
return new InitializeProcessor($this);
},
@@ -260,58 +294,139 @@ class Grav extends Container
]
);
- $default = function (ServerRequestInterface $request) {
- return new Response(404);
+ $default = static function () {
+ return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
};
- /** @var Debugger $debugger */
- $debugger = $this['debugger'];
-
$collection = new RequestHandler($this->middleware, $default, $container);
$response = $collection->handle($this['request']);
$body = $response->getBody();
+ /** @var Messages $messages */
+ $messages = $this['messages'];
+
+ // Prevent caching if session messages were displayed in the page.
+ $noCache = $messages->isCleared();
+ if ($noCache) {
+ $response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
+ }
+
// Handle ETag and If-None-Match headers.
if ($response->getHeaderLine('ETag') === '1') {
$etag = md5($body);
- $response = $response->withHeader('ETag', $etag);
+ $response = $response->withHeader('ETag', '"' . $etag . '"');
- if ($this['request']->getHeaderLine('If-None-Match') === $etag) {
+ $search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
+ if ($noCache === false && $search === $etag) {
$response = $response->withStatus(304);
$body = '';
}
}
+ // Echo page content.
$this->header($response);
echo $body;
- $debugger->render();
+ $this['debugger']->render();
- register_shutdown_function([$this, 'shutdown']);
+ // Response object can turn off all shutdown processing. This can be used for example to speed up AJAX responses.
+ // Note that using this feature will also turn off response compression.
+ if ($response->getHeaderLine('Grav-Internal-SkipShutdown') !== '1') {
+ register_shutdown_function([$this, 'shutdown']);
+ }
}
/**
- * Set the system locale based on the language and configuration
+ * Terminates Grav request with a response.
+ *
+ * Please use this method instead of calling `die();` or `exit();`. Note that you need to create a response object.
+ *
+ * @param ResponseInterface $response
+ * @return void
*/
- public function setLocale()
+ public function close(ResponseInterface $response): void
{
- // Initialize Locale if set and configured.
- if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
- $language = $this['language']->getLanguage();
- setlocale(LC_ALL, \strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
- } elseif ($this['config']->get('system.default_locale')) {
- setlocale(LC_ALL, $this['config']->get('system.default_locale'));
+ // Make sure nothing extra gets written to the response.
+ while (ob_get_level()) {
+ ob_end_clean();
+ }
+
+ // Close the session.
+ if (isset($this['session'])) {
+ $this['session']->close();
+ }
+
+ /** @var ServerRequestInterface $request */
+ $request = $this['request'];
+
+ /** @var Debugger $debugger */
+ $debugger = $this['debugger'];
+ $response = $debugger->logRequest($request, $response);
+
+ $body = $response->getBody();
+
+ /** @var Messages $messages */
+ $messages = $this['messages'];
+
+ // Prevent caching if session messages were displayed in the page.
+ $noCache = $messages->isCleared();
+ if ($noCache) {
+ $response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
+ }
+
+ // Handle ETag and If-None-Match headers.
+ if ($response->getHeaderLine('ETag') === '1') {
+ $etag = md5($body);
+ $response = $response->withHeader('ETag', '"' . $etag . '"');
+
+ $search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
+ if ($noCache === false && $search === $etag) {
+ $response = $response->withStatus(304);
+ $body = '';
+ }
}
+
+ // Echo page content.
+ $this->header($response);
+ echo $body;
+ exit();
}
/**
- * Redirect browser to another location.
+ * @param ResponseInterface $response
+ * @return void
+ * @deprecated 1.7 Do not use
+ */
+ public function exit(ResponseInterface $response): void
+ {
+ $this->close($response);
+ }
+
+ /**
+ * Terminates Grav request and redirects browser to another location.
+ *
+ * Please use this method instead of calling `header("Location: {$url}", true, 302); exit();`.
*
* @param string $route Internal route.
- * @param int $code Redirection code (30x)
+ * @param int|null $code Redirection code (30x)
+ * @return void
+ */
+ public function redirect($route, $code = null): void
+ {
+ $response = $this->getRedirectResponse($route, $code);
+
+ $this->close($response);
+ }
+
+ /**
+ * Returns redirect response object from Grav.
+ *
+ * @param string $route Internal route.
+ * @param int|null $code Redirection code (30x)
+ * @return ResponseInterface
*/
- public function redirect($route, $code = null)
+ public function getRedirectResponse($route, $code = null): ResponseInterface
{
/** @var Uri $uri */
$uri = $this['uri'];
@@ -319,23 +434,25 @@ class Grav extends Container
// Clean route for redirect
$route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
- // Check for code in route
- $regex = '/.*(\[(30[1-7])\])$/';
- preg_match($regex, $route, $matches);
- if ($matches) {
- $route = str_replace($matches[1], '', $matches[0]);
- $code = $matches[2];
+ if ($code < 300 || $code > 399) {
+ $code = null;
}
- if ($code === null) {
- $code = $this['config']->get('system.pages.redirect_default_code', 302);
+ if (null === $code) {
+ // Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html
+ $regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/';
+ preg_match($regex, $route, $matches);
+ if ($matches) {
+ $route = str_replace($matches[1], '', $matches[0]);
+ $code = $matches[2];
+ }
}
- if (isset($this['session'])) {
- $this['session']->close();
+ if ($code === null) {
+ $code = $this['config']->get('system.pages.redirect_default_code', 302);
}
- if ($uri->isExternal($route)) {
+ if ($uri::isExternal($route)) {
$url = $route;
} else {
$url = rtrim($uri->rootUrl(), '/') . '/';
@@ -347,8 +464,7 @@ class Grav extends Container
}
}
- header("Location: {$url}", true, $code);
- exit();
+ return new Response($code, ['Location' => $url]);
}
/**
@@ -356,8 +472,9 @@ class Grav extends Container
*
* @param string $route Internal route.
* @param int $code Redirection code (30x)
+ * @return void
*/
- public function redirectLangSafe($route, $code = null)
+ public function redirectLangSafe($route, $code = null): void
{
if (!$this['uri']->isExternal($route)) {
$this->redirect($this['pages']->route($route), $code);
@@ -370,8 +487,9 @@ class Grav extends Container
* Set response header.
*
* @param ResponseInterface|null $response
+ * @return void
*/
- public function header(ResponseInterface $response = null)
+ public function header(ResponseInterface $response = null): void
{
if (null === $response) {
/** @var PageInterface $page */
@@ -381,36 +499,86 @@ class Grav extends Container
header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
foreach ($response->getHeaders() as $key => $values) {
+ // Skip internal Grav headers.
+ if (strpos($key, 'Grav-Internal-') === 0) {
+ continue;
+ }
foreach ($values as $i => $value) {
header($key . ': ' . $value, $i === 0);
}
}
}
+ /**
+ * Set the system locale based on the language and configuration
+ *
+ * @return void
+ */
+ public function setLocale(): void
+ {
+ // Initialize Locale if set and configured.
+ if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
+ $language = $this['language']->getLanguage();
+ setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
+ } elseif ($this['config']->get('system.default_locale')) {
+ setlocale(LC_ALL, $this['config']->get('system.default_locale'));
+ }
+ }
+
+ /**
+ * @param object $event
+ * @return object
+ */
+ public function dispatchEvent($event)
+ {
+ /** @var EventDispatcherInterface $events */
+ $events = $this['events'];
+ $eventName = get_class($event);
+
+ $timestamp = microtime(true);
+ $event = $events->dispatch($event);
+
+ /** @var Debugger $debugger */
+ $debugger = $this['debugger'];
+ $debugger->addEvent($eventName, $event, $events, $timestamp);
+
+ return $event;
+ }
+
/**
* Fires an event with optional parameters.
*
* @param string $eventName
- * @param Event $event
- *
+ * @param Event|null $event
* @return Event
*/
public function fireEvent($eventName, Event $event = null)
{
- /** @var EventDispatcher $events */
+ /** @var EventDispatcherInterface $events */
$events = $this['events'];
+ if (null === $event) {
+ $event = new Event();
+ }
- return $events->dispatch($eventName, $event);
+ $timestamp = microtime(true);
+ $events->dispatch($event, $eventName);
+
+ /** @var Debugger $debugger */
+ $debugger = $this['debugger'];
+ $debugger->addEvent($eventName, $event, $events, $timestamp);
+
+ return $event;
}
/**
* Set the final content length for the page and flush the buffer
*
+ * @return void
*/
- public function shutdown()
+ public function shutdown(): void
{
// Prevent user abort allowing onShutdown event to run without interruptions.
- if (\function_exists('ignore_user_abort')) {
+ if (function_exists('ignore_user_abort')) {
@ignore_user_abort(true);
}
@@ -419,32 +587,33 @@ class Grav extends Container
$this['session']->close();
}
- if ($this['config']->get('system.debugger.shutdown.close_connection', true)) {
+ /** @var Config $config */
+ $config = $this['config'];
+ if ($config->get('system.debugger.shutdown.close_connection', true)) {
// Flush the response and close the connection to allow time consuming tasks to be performed without leaving
// the connection to the client open. This will make page loads to feel much faster.
// FastCGI allows us to flush all response data to the client and finish the request.
- $success = \function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
-
+ $success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
if (!$success) {
// Unfortunately without FastCGI there is no way to force close the connection.
// We need to ask browser to close the connection for us.
- if ($this['config']->get('system.cache.gzip')) {
- // Flush gzhandler buffer if gzip setting was enabled.
- ob_end_flush();
- } else {
+ if ($config->get('system.cache.gzip')) {
+ // Flush gzhandler buffer if gzip setting was enabled to get the size of the compressed output.
+ ob_end_flush();
+ } elseif ($config->get('system.cache.allow_webserver_gzip')) {
+ // Let web server to do the hard work.
+ header('Content-Encoding: identity');
+ } elseif (function_exists('apache_setenv')) {
// Without gzip we have no other choice than to prevent server from compressing the output.
// This action turns off mod_deflate which would prevent us from closing the connection.
- if ($this['config']->get('system.cache.allow_webserver_gzip')) {
- header('Content-Encoding: identity');
- } else {
- header('Content-Encoding: none');
- }
-
+ @apache_setenv('no-gzip', '1');
+ } else {
+ // Fall back to unknown content encoding, it prevents most servers from deflating the content.
+ header('Content-Encoding: none');
}
-
// Get length and close the connection.
header('Content-Length: ' . ob_get_length());
header('Connection: close');
@@ -468,7 +637,7 @@ class Grav extends Container
*
* @param string $method
* @param array $args
- * @return
+ * @return mixed|null
*/
public function __call($method, $args)
{
@@ -499,7 +668,6 @@ class Grav extends Container
* Initialize and return a Grav instance
*
* @param array $values
- *
* @return static
*/
protected static function load(array $values)
@@ -513,9 +681,7 @@ class Grav extends Container
return $container;
};
- $container->measureTime('_services', 'Services', function () use ($container) {
- $container->registerServices();
- });
+ $container->registerServices();
return $container;
}
@@ -528,10 +694,10 @@ class Grav extends Container
*
* @return void
*/
- protected function registerServices()
+ protected function registerServices(): void
{
foreach (self::$diMap as $serviceKey => $serviceClass) {
- if (\is_int($serviceKey)) {
+ if (is_int($serviceKey)) {
$this->register(new $serviceClass);
} else {
$this[$serviceKey] = function ($c) use ($serviceClass) {
@@ -545,6 +711,7 @@ class Grav extends Container
* This attempts to find media, other files, and download them
*
* @param string $path
+ * @return PageInterface|false
*/
public function fallbackUrl($path)
{
@@ -561,7 +728,7 @@ class Grav extends Container
$supported_types = $config->get('media.types');
// Check whitelist first, then ensure extension is a valid media type
- if (!empty($fallback_types) && !\in_array($uri_extension, $fallback_types, true)) {
+ if (!empty($fallback_types) && !in_array($uri_extension, $fallback_types, true)) {
return false;
}
if (!array_key_exists($uri_extension, $supported_types)) {
@@ -570,8 +737,9 @@ class Grav extends Container
$path_parts = pathinfo($path);
- /** @var PageInterface $page */
- $page = $this['pages']->dispatch($path_parts['dirname'], true);
+ /** @var Pages $pages */
+ $pages = $this['pages'];
+ $page = $pages->find($path_parts['dirname'], true);
if ($page) {
$media = $page->media()->all();
@@ -583,8 +751,8 @@ class Grav extends Container
/** @var Medium $medium */
$medium = $media[$media_file];
foreach ($uri->query(null, true) as $action => $params) {
- if (\in_array($action, ImageMedium::$magic_actions, true)) {
- \call_user_func_array([&$medium, $action], explode(',', $params));
+ if (in_array($action, ImageMedium::$magic_actions, true)) {
+ call_user_func_array([&$medium, $action], explode(',', $params));
}
}
Utils::download($medium->path(), false);
@@ -593,17 +761,15 @@ class Grav extends Container
// unsupported media type, try to download it...
if ($uri_extension) {
$extension = $uri_extension;
+ } elseif (isset($path_parts['extension'])) {
+ $extension = $path_parts['extension'];
} else {
- if (isset($path_parts['extension'])) {
- $extension = $path_parts['extension'];
- } else {
- $extension = null;
- }
+ $extension = null;
}
if ($extension) {
$download = true;
- if (\in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
+ if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
$download = false;
}
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
@@ -613,6 +779,6 @@ class Grav extends Container
return false;
}
- return $page;
+ return $page ?? false;
}
}
diff --git a/system/src/Grav/Common/GravTrait.php b/system/src/Grav/Common/GravTrait.php
index a82c23a9..46389057 100644
--- a/system/src/Grav/Common/GravTrait.php
+++ b/system/src/Grav/Common/GravTrait.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,6 +14,7 @@ namespace Grav\Common;
*/
trait GravTrait
{
+ /** @var Grav */
protected static $grav;
/**
@@ -24,7 +25,7 @@ trait GravTrait
{
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
- if (!self::$grav) {
+ if (null === self::$grav) {
self::$grav = Grav::instance();
}
diff --git a/system/src/Grav/Common/Helpers/Base32.php b/system/src/Grav/Common/Helpers/Base32.php
index f1385570..825c1eca 100644
--- a/system/src/Grav/Common/Helpers/Base32.php
+++ b/system/src/Grav/Common/Helpers/Base32.php
@@ -3,15 +3,26 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
+use function chr;
+use function count;
+use function ord;
+use function strlen;
+
+/**
+ * Class Base32
+ * @package Grav\Common\Helpers
+ */
class Base32
{
+ /** @var string */
protected static $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
+ /** @var array */
protected static $base32Lookup = [
0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
@@ -33,17 +44,18 @@ class Base32
*/
public static function encode($bytes)
{
- $i = 0; $index = 0;
+ $i = 0;
+ $index = 0;
$base32 = '';
- $bytesLen = \strlen($bytes);
+ $bytesLen = strlen($bytes);
while ($i < $bytesLen) {
- $currByte = \ord($bytes[$i]);
+ $currByte = ord($bytes[$i]);
/* Is the current digit going to span a byte boundary? */
if ($index > 3) {
if (($i + 1) < $bytesLen) {
- $nextByte = \ord($bytes[$i+1]);
+ $nextByte = ord($bytes[$i+1]);
} else {
$nextByte = 0;
}
@@ -75,15 +87,15 @@ class Base32
public static function decode($base32)
{
$bytes = [];
- $base32Len = \strlen($base32);
- $base32LookupLen = \count(self::$base32Lookup);
+ $base32Len = strlen($base32);
+ $base32LookupLen = count(self::$base32Lookup);
for ($i = $base32Len * 5 / 8 - 1; $i >= 0; --$i) {
$bytes[] = 0;
}
for ($i = 0, $index = 0, $offset = 0; $i < $base32Len; $i++) {
- $lookup = \ord($base32[$i]) - \ord('0');
+ $lookup = ord($base32[$i]) - ord('0');
/* Skip chars outside the lookup table */
if ($lookup < 0 || $lookup >= $base32LookupLen) {
@@ -102,7 +114,7 @@ class Base32
if ($index === 0) {
$bytes[$offset] |= $digit;
$offset++;
- if ($offset >= \count($bytes)) {
+ if ($offset >= count($bytes)) {
break;
}
} else {
@@ -112,7 +124,7 @@ class Base32
$index = ($index + 5) % 8;
$bytes[$offset] |= ($digit >> $index);
$offset++;
- if ($offset >= \count($bytes)) {
+ if ($offset >= count($bytes)) {
break;
}
$bytes[$offset] |= $digit << (8 - $index);
@@ -121,7 +133,7 @@ class Base32
$bites = '';
foreach ($bytes as $byte) {
- $bites .= \chr($byte);
+ $bites .= chr($byte);
}
return $bites;
diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php
index f07c3aa5..173850a5 100644
--- a/system/src/Grav/Common/Helpers/Excerpts.php
+++ b/system/src/Grav/Common/Helpers/Excerpts.php
@@ -3,16 +3,24 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
+use DOMDocument;
+use DOMElement;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject;
+use Grav\Common\Page\Medium\Link;
use Grav\Common\Page\Medium\Medium;
+use function is_array;
+/**
+ * Class Excerpts
+ * @package Grav\Common\Helpers
+ */
class Excerpts
{
/**
@@ -25,6 +33,9 @@ class Excerpts
public static function processImageHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
+ if (null === $excerpt) {
+ return '';
+ }
$original_src = $excerpt['element']['attributes']['src'];
$excerpt['element']['attributes']['href'] = $original_src;
@@ -32,7 +43,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 +54,29 @@ class Excerpts
return $html;
}
+ /**
+ * Process Grav page link URL from HTML tag
+ *
+ * @param string $html HTML tag e.g. `Page Link`
+ * @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');
+ if (null === $excerpt) {
+ return '';
+ }
+
+ $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 +86,34 @@ class Excerpts
*/
public static function getExcerptFromHtml($html, $tag)
{
- $doc = new \DOMDocument();
- $doc->loadHTML($html);
- $images = $doc->getElementsByTagName($tag);
+ $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);
+
+ $elements = $doc->getElementsByTagName($tag);
$excerpt = null;
+ $inner = [];
- foreach ($images as $image) {
+ 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 +141,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 .= ' />';
@@ -139,7 +185,7 @@ class Excerpts
* @param Medium $medium
* @param string|array $url
* @param PageInterface|null $page Page, defaults to the current page object
- * @return Medium
+ * @return Medium|Link
*/
public static function processMediaActions($medium, $url, PageInterface $page = null)
{
diff --git a/system/src/Grav/Common/Helpers/Exif.php b/system/src/Grav/Common/Helpers/Exif.php
index 15aff667..36e391fe 100644
--- a/system/src/Grav/Common/Helpers/Exif.php
+++ b/system/src/Grav/Common/Helpers/Exif.php
@@ -3,39 +3,46 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
+use PHPExif\Reader\Reader;
+use RuntimeException;
+use function function_exists;
+/**
+ * Class Exif
+ * @package Grav\Common\Helpers
+ */
class Exif
{
+ /** @var Reader */
public $reader;
/**
* Exif constructor.
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public function __construct()
{
if (Grav::instance()['config']->get('system.media.auto_metadata_exif')) {
- if (function_exists('exif_read_data') && class_exists('\PHPExif\Reader\Reader')) {
- $this->reader = \PHPExif\Reader\Reader::factory(\PHPExif\Reader\Reader::TYPE_NATIVE);
+ if (function_exists('exif_read_data') && class_exists(Reader::class)) {
+ $this->reader = Reader::factory(Reader::TYPE_NATIVE);
} else {
- throw new \RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration');
+ throw new RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration');
}
}
}
+ /**
+ * @return Reader
+ */
public function getReader()
{
- if ($this->reader) {
- return $this->reader;
- }
-
- return false;
+ return $this->reader;
}
}
diff --git a/system/src/Grav/Common/Helpers/LogViewer.php b/system/src/Grav/Common/Helpers/LogViewer.php
index 397fb175..187828ae 100644
--- a/system/src/Grav/Common/Helpers/LogViewer.php
+++ b/system/src/Grav/Common/Helpers/LogViewer.php
@@ -3,14 +3,24 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
+use DateTime;
+use function array_slice;
+use function is_array;
+use function is_string;
+
+/**
+ * Class LogViewer
+ * @package Grav\Common\Helpers
+ */
class LogViewer
{
+ /** @var string */
protected $pattern = '/\[(?P.*)\] (?P\w+).(?P\w+): (?P.*[^ ]+) (?P[^ ]+) (?P[^ ]+)/';
/**
@@ -24,7 +34,7 @@ class LogViewer
public function objectTail($filepath, $lines = 1, $desc = true)
{
$data = $this->tail($filepath, $lines);
- $tailed_log = explode(PHP_EOL, $data);
+ $tailed_log = $data ? explode(PHP_EOL, $data) : [];
$line_objects = [];
foreach ($tailed_log as $line) {
@@ -39,21 +49,24 @@ class LogViewer
*
* @param string $filepath
* @param int $lines
- * @return bool|string
+ * @return string|false
*/
- public function tail($filepath, $lines = 1) {
-
- $f = @fopen($filepath, "rb");
- if ($f === false) return false;
+ public function tail($filepath, $lines = 1)
+ {
+ $f = $filepath ? @fopen($filepath, 'rb') : false;
+ if ($f === false) {
+ return false;
+ }
- else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
+ $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
fseek($f, -1, SEEK_END);
- if (fread($f, 1) != "\n") $lines -= 1;
+ if (fread($f, 1) !== "\n") {
+ --$lines;
+ }
// Start reading
$output = '';
- $chunk = '';
// While we would like more
while (ftell($f) > 0 && $lines >= 0) {
// Figure out how far back we should jump
@@ -61,7 +74,11 @@ class LogViewer
// Do the jump (backwards, relative to where we are)
fseek($f, -$seek, SEEK_CUR);
// Read a chunk and prepend it to our output
- $output = ($chunk = fread($f, $seek)) . $output;
+ $chunk = fread($f, $seek);
+ if ($chunk === false) {
+ throw new \RuntimeException('Cannot read file');
+ }
+ $output = $chunk . $output;
// Jump back to where we started reading
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
// Decrease our line counter
@@ -83,7 +100,7 @@ class LogViewer
* Helper class to get level color
*
* @param string $level
- * @return mixed|string
+ * @return string
*/
public static function levelColor($level)
{
@@ -108,12 +125,13 @@ class LogViewer
*/
public function parse($line)
{
- if( !is_string($line) || strlen($line) === 0) {
- return array();
+ if (!is_string($line) || $line === '') {
+ return [];
}
+
preg_match($this->pattern, $line, $data);
if (!isset($data['date'])) {
- return array();
+ return [];
}
preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
@@ -122,15 +140,15 @@ class LogViewer
$data['trace'] = trim($matches[2]);
}
- return array(
- 'date' => \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
+ return [
+ 'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
'logger' => $data['logger'],
'level' => $data['level'],
'message' => $data['message'],
'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
'context' => json_decode($data['context'], true),
'extra' => json_decode($data['extra'], true)
- );
+ ];
}
/**
@@ -143,7 +161,7 @@ class LogViewer
public static function parseTrace($trace, $rows = 10)
{
$lines = array_filter(preg_split('/#\d*/m', $trace));
+
return array_slice($lines, 0, $rows);
}
-
}
diff --git a/system/src/Grav/Common/Helpers/Truncator.php b/system/src/Grav/Common/Helpers/Truncator.php
index d116bb34..0a701b69 100644
--- a/system/src/Grav/Common/Helpers/Truncator.php
+++ b/system/src/Grav/Common/Helpers/Truncator.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,6 +15,8 @@ use DOMElement;
use DOMNode;
use DOMWordsIterator;
use DOMLettersIterator;
+use function in_array;
+use function strlen;
/**
* This file is part of https://github.com/Bluetel-Solutions/twig-truncate-extension
@@ -25,11 +27,11 @@ use DOMLettersIterator;
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-
-class Truncator {
-
+class Truncator
+{
/**
* Safely truncates HTML by a given number of words.
+ *
* @param string $html Input HTML.
* @param int $limit Limit to how many words we preserve.
* @param string $ellipsis String to use as ellipsis (if any).
@@ -49,10 +51,8 @@ class Truncator {
$words = new DOMWordsIterator($container);
$truncated = false;
foreach ($words as $word) {
-
// If we have exceeded the limit, we delete the remainder of the content.
if ($words->key() >= $limit) {
-
// Grab current position.
$currentWordPosition = $words->currentWordPosition();
$curNode = $currentWordPosition[0];
@@ -75,7 +75,6 @@ class Truncator {
break;
}
-
}
// Return original HTML if not truncated.
@@ -88,6 +87,7 @@ class Truncator {
/**
* Safely truncates HTML by a given number of letters.
+ *
* @param string $html Input HTML.
* @param int $limit Limit to how many letters we preserve.
* @param string $ellipsis String to use as ellipsis (if any).
@@ -107,10 +107,8 @@ class Truncator {
$letters = new DOMLettersIterator($container);
$truncated = false;
foreach ($letters as $letter) {
-
// If we have exceeded the limit, we want to delete the remainder of this document.
if ($letters->key() >= $limit) {
-
$currentText = $letters->currentTextPosition();
$currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
self::removeProceedingNodes($currentText[0], $container);
@@ -135,8 +133,9 @@ class Truncator {
/**
* Builds a DOMDocument object from a string containing HTML.
+ *
* @param string $html HTML to load
- * @returns DOMDocument Returns a DOMDocument object.
+ * @return DOMDocument Returns a DOMDocument object.
*/
public static function htmlToDomDocument($html)
{
@@ -160,12 +159,14 @@ class Truncator {
/**
* Removes all nodes after the current node.
+ *
* @param DOMNode|DOMElement $domNode
* @param DOMNode|DOMElement $topNode
* @return void
*/
private static function removeProceedingNodes($domNode, $topNode)
{
+ /** @var DOMNode|null $nextNode */
$nextNode = $domNode->nextSibling;
if ($nextNode !== null) {
@@ -190,25 +191,25 @@ class Truncator {
* Clean extra code
*
* @param DOMDocument $doc
- * @param $container
+ * @param DOMNode $container
* @return string
*/
- private static function getCleanedHTML(DOMDocument $doc, $container)
+ private static function getCleanedHTML(DOMDocument $doc, DOMNode $container)
{
while ($doc->firstChild) {
$doc->removeChild($doc->firstChild);
}
- while ($container->firstChild ) {
+ while ($container->firstChild) {
$doc->appendChild($container->firstChild);
}
- $html = trim($doc->saveHTML());
- return $html;
+ return trim($doc->saveHTML());
}
/**
* Inserts an ellipsis
+ *
* @param DOMNode|DOMElement $domNode Element to insert after.
* @param string $ellipsis Text used to suffix our document.
* @return void
@@ -221,12 +222,13 @@ class Truncator {
// Append as text node to parent instead
$textNode = new DOMText($ellipsis);
- if ($domNode->parentNode->parentNode->nextSibling) {
+ /** @var DOMNode|null $nextSibling */
+ $nextSibling = $domNode->parentNode->parentNode->nextSibling;
+ if ($nextSibling) {
$domNode->parentNode->parentNode->insertBefore($textNode, $domNode->parentNode->parentNode->nextSibling);
} else {
$domNode->parentNode->parentNode->appendChild($textNode);
}
-
} else {
// Append to current node
$domNode->nodeValue = rtrim($domNode->nodeValue) . $ellipsis;
@@ -234,7 +236,12 @@ class Truncator {
}
/**
- *
+ * @param string $text
+ * @param int $length
+ * @param string $ending
+ * @param bool $exact
+ * @param bool $considerHtml
+ * @return string
*/
public function truncate(
$text,
@@ -248,11 +255,13 @@ class Truncator {
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text;
}
+
// splits all html-tags to scanable lines
preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
$total_length = strlen($ending);
- $open_tags = array();
$truncate = '';
+ $open_tags = [];
+
foreach ($lines as $line_matchings) {
// if there is any html-tag in this line, handle it and add it (uncounted) to the output
if (!empty($line_matchings[1])) {
@@ -260,14 +269,14 @@ class Truncator {
if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
// do nothing
// if tag is a closing tag
- } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
+ } elseif (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
// delete tag from $open_tags list
$pos = array_search($tag_matchings[1], $open_tags);
if ($pos !== false) {
unset($open_tags[$pos]);
}
// if tag is an opening tag
- } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
+ } elseif (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
// add tag to the beginning of $open_tags list
array_unshift($open_tags, strtolower($tag_matchings[1]));
}
@@ -301,35 +310,35 @@ class Truncator {
$total_length += $content_length;
}
// if the maximum length is reached, get off the loop
- if($total_length>= $length) {
+ if ($total_length>= $length) {
break;
}
}
} else {
if (strlen($text) <= $length) {
return $text;
- } else {
- $truncate = substr($text, 0, $length - strlen($ending));
}
+
+ $truncate = substr($text, 0, $length - strlen($ending));
}
// if the words shouldn't be cut in the middle...
if (!$exact) {
// ...search the last occurance of a space...
$spacepos = strrpos($truncate, ' ');
- if (isset($spacepos)) {
+ if (false !== $spacepos) {
// ...and cut the text in this position
$truncate = substr($truncate, 0, $spacepos);
}
}
// add the defined ending to the text
$truncate .= $ending;
- if($considerHtml) {
+ if (isset($open_tags)) {
// close all unclosed html-tags
foreach ($open_tags as $tag) {
$truncate .= '' . $tag . '>';
}
}
+
return $truncate;
}
-
}
diff --git a/system/src/Grav/Common/Helpers/YamlLinter.php b/system/src/Grav/Common/Helpers/YamlLinter.php
index b7e608d0..45b5144e 100644
--- a/system/src/Grav/Common/Helpers/YamlLinter.php
+++ b/system/src/Grav/Common/Helpers/YamlLinter.php
@@ -3,38 +3,61 @@
/**
* @package Grav\Common\Helpers
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
+use Exception;
use Grav\Common\Grav;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
use RocketTheme\Toolbox\File\MarkdownFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Yaml\Yaml;
+/**
+ * Class YamlLinter
+ * @package Grav\Common\Helpers
+ */
class YamlLinter
{
- public static function lint()
+ /**
+ * @param string|null $folder
+ * @return array
+ */
+ public static function lint(string $folder = null)
{
- $errors = static::lintConfig();
- $errors = $errors + static::lintPages();
- $errors = $errors + static::lintBlueprints();
-
- return $errors;
+ if (null !== $folder) {
+ $folder = $folder ?: GRAV_ROOT;
+
+ return static::recurseFolder($folder);
+ }
+
+ return array_merge(static::lintConfig(), static::lintPages(), static::lintBlueprints());
}
+ /**
+ * @return array
+ */
public static function lintPages()
{
return static::recurseFolder('page://');
}
+ /**
+ * @return array
+ */
public static function lintConfig()
{
return static::recurseFolder('config://');
}
+ /**
+ * @return array
+ */
public static function lintBlueprints()
{
/** @var UniformResourceLocator $locator */
@@ -47,26 +70,31 @@ class YamlLinter
return static::recurseFolder('blueprints://');
}
- public static function recurseFolder($path, $extensions = 'md|yaml')
+ /**
+ * @param string $path
+ * @param string $extensions
+ * @return array
+ */
+ public static function recurseFolder($path, $extensions = '(md|yaml)')
{
$lint_errors = [];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
+ $flags = RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
- $directory = new \RecursiveDirectoryIterator($path, $flags);
+ $directory = new RecursiveDirectoryIterator($path, $flags);
}
- $recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
- $iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
+ $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
+ $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/ui');
- /** @var \RecursiveDirectoryIterator $file */
+ /** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $filepath => $file) {
try {
Yaml::parse(static::extractYaml($filepath));
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage();
}
}
@@ -74,16 +102,20 @@ class YamlLinter
return $lint_errors;
}
+ /**
+ * @param string $path
+ * @return string
+ */
protected static function extractYaml($path)
{
$extension = pathinfo($path, PATHINFO_EXTENSION);
if ($extension === 'md') {
$file = MarkdownFile::instance($path);
$contents = $file->frontmatter();
+ $file->free();
} else {
$contents = file_get_contents($path);
}
return $contents;
}
-
}
diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php
index eca93c19..50c218f1 100644
--- a/system/src/Grav/Common/Inflector.php
+++ b/system/src/Grav/Common/Inflector.php
@@ -3,33 +3,53 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use DateInterval;
+use DateTime;
+use Grav\Common\Language\Language;
+use function in_array;
+use function is_array;
+use function strlen;
+
/**
* This file was originally part of the Akelos Framework
*/
-
class Inflector
{
+ /** @var bool */
+ protected static $initialized = false;
+ /** @var array|null */
protected static $plural;
+ /** @var array|null */
protected static $singular;
+ /** @var array|null */
protected static $uncountable;
+ /** @var array|null */
protected static $irregular;
+ /** @var array|null */
protected static $ordinals;
+ /**
+ * @return void
+ */
public static function init()
{
- if (empty(static::$plural)) {
+ if (!static::$initialized) {
+ static::$initialized = true;
+ /** @var Language $language */
$language = Grav::instance()['language'];
- static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true) ?: [];
- static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true) ?: [];
- static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true) ?: [];
- static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true) ?: [];
- static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true) ?: [];
+ if (!$language->isDebug()) {
+ static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true);
+ static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true);
+ static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true);
+ static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true);
+ static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true);
+ }
}
}
@@ -38,8 +58,7 @@ class Inflector
*
* @param string $word English noun to pluralize
* @param int $count The count
- *
- * @return string Plural noun
+ * @return string|false Plural noun
*/
public static function pluralize($word, $count = 2)
{
@@ -51,26 +70,31 @@ class Inflector
$lowercased_word = strtolower($word);
- foreach (static::$uncountable as $_uncountable) {
- if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
- return $word;
+ if (is_array(static::$uncountable)) {
+ foreach (static::$uncountable as $_uncountable) {
+ if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
+ return $word;
+ }
}
}
- foreach (static::$irregular as $_plural => $_singular) {
- if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
- return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
+ if (is_array(static::$irregular)) {
+ foreach (static::$irregular as $_plural => $_singular) {
+ if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
+ return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
+ }
}
}
- foreach (static::$plural as $rule => $replacement) {
- if (preg_match($rule, $word)) {
- return preg_replace($rule, $replacement, $word);
+ if (is_array(static::$plural)) {
+ foreach (static::$plural as $rule => $replacement) {
+ if (preg_match($rule, $word)) {
+ return preg_replace($rule, $replacement, $word);
+ }
}
}
return false;
-
}
/**
@@ -90,21 +114,28 @@ class Inflector
}
$lowercased_word = strtolower($word);
- foreach (static::$uncountable as $_uncountable) {
- if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
- return $word;
+
+ if (is_array(static::$uncountable)) {
+ foreach (static::$uncountable as $_uncountable) {
+ if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
+ return $word;
+ }
}
}
- foreach (static::$irregular as $_plural => $_singular) {
- if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
- return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
+ if (is_array(static::$irregular)) {
+ foreach (static::$irregular as $_plural => $_singular) {
+ if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
+ return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
+ }
}
}
- foreach (static::$singular as $rule => $replacement) {
- if (preg_match($rule, $word)) {
- return preg_replace($rule, $replacement, $word);
+ if (is_array(static::$singular)) {
+ foreach (static::$singular as $rule => $replacement) {
+ if (preg_match($rule, $word)) {
+ return preg_replace($rule, $replacement, $word);
+ }
}
}
@@ -144,8 +175,7 @@ class Inflector
*
* @see variablize
*
- * @param string $word Word to convert to camel case
- *
+ * @param string $word Word to convert to camel case
* @return string UpperCamelCasedWord
*/
public static function camelize($word)
@@ -161,8 +191,7 @@ class Inflector
*
* This can be really useful for creating friendly URLs.
*
- * @param string $word Word to underscore
- *
+ * @param string $word Word to underscore
* @return string Underscored word
*/
public static function underscorize($word)
@@ -182,8 +211,7 @@ class Inflector
*
* This can be really useful for creating friendly URLs.
*
- * @param string $word Word to hyphenate
- *
+ * @param string $word Word to hyphenate
* @return string hyphenized word
*/
public static function hyphenize($word)
@@ -230,8 +258,7 @@ class Inflector
*
* @see camelize
*
- * @param string $word Word to lowerCamelCase
- *
+ * @param string $word Word to lowerCamelCase
* @return string Returns a lowerCamelCasedWord
*/
public static function variablize($word)
@@ -249,8 +276,7 @@ class Inflector
*
* @see classify
*
- * @param string $class_name Class name for getting related table_name.
- *
+ * @param string $class_name Class name for getting related table_name.
* @return string plural_table_name
*/
public static function tableize($class_name)
@@ -266,8 +292,7 @@ class Inflector
*
* @see tableize
*
- * @param string $table_name Table name for getting related ClassName.
- *
+ * @param string $table_name Table name for getting related ClassName.
* @return string SingularClassName
*/
public static function classify($table_name)
@@ -280,15 +305,18 @@ class Inflector
*
* This method converts 13 to 13th, 2 to 2nd ...
*
- * @param int $number Number to get its ordinal value
- *
+ * @param int $number Number to get its ordinal value
* @return string Ordinal representation of given string.
*/
public static function ordinalize($number)
{
+ if (!is_array(static::$ordinals)) {
+ return (string)$number;
+ }
+
static::init();
- if (\in_array($number % 100, range(11, 13), true)) {
+ if (in_array($number % 100, range(11, 13), true)) {
return $number . static::$ordinals['default'];
}
@@ -308,15 +336,14 @@ class Inflector
* Converts a number of days to a number of months
*
* @param int $days
- *
* @return int
*/
public static function monthize($days)
{
- $now = new \DateTime();
- $end = new \DateTime();
+ $now = new DateTime();
+ $end = new DateTime();
- $duration = new \DateInterval("P{$days}D");
+ $duration = new DateInterval("P{$days}D");
$diff = $end->add($duration)->diff($now);
diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php
index 5f6dc7ea..6e80b947 100644
--- a/system/src/Grav/Common/Iterator.php
+++ b/system/src/Grav/Common/Iterator.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,14 +15,20 @@ use RocketTheme\Toolbox\ArrayTraits\Constructor;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\Serializable;
+use function array_slice;
+use function count;
+use function is_callable;
+use function is_object;
+/**
+ * Class Iterator
+ * @package Grav\Common
+ */
class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
{
use Constructor, ArrayAccessWithGetters, ArrayIterator, Countable, Serializable, Export;
- /**
- * @var array
- */
+ /** @var array */
protected $items = [];
/**
@@ -30,7 +36,6 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
*
* @param string $key
* @param mixed $args
- *
* @return mixed
*/
public function __call($key, $args)
@@ -44,7 +49,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
public function __clone()
{
foreach ($this as $key => $value) {
- if (\is_object($value)) {
+ if (is_object($value)) {
$this->{$key} = clone $this->{$key};
}
}
@@ -64,6 +69,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Remove item from the list.
*
* @param string $key
+ * @return void
*/
public function remove($key)
{
@@ -84,7 +90,6 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Return nth item.
*
* @param int $key
- *
* @return mixed|bool
*/
public function nth($key)
@@ -133,7 +138,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
/**
* @param mixed $needle Searched value.
*
- * @return string|bool Key if found, otherwise false.
+ * @return string|int|false Key if found, otherwise false.
*/
public function indexOf($needle)
{
@@ -170,13 +175,12 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Slice the list.
*
* @param int $offset
- * @param int $length
- *
+ * @param int|null $length
* @return $this
*/
public function slice($offset, $length = null)
{
- $this->items = \array_slice($this->items, $offset, $length);
+ $this->items = array_slice($this->items, $offset, $length);
return $this;
}
@@ -185,12 +189,11 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Pick one or more random entries.
*
* @param int $num Specifies how many entries should be picked.
- *
* @return $this
*/
public function random($num = 1)
{
- $count = \count($this->items);
+ $count = count($this->items);
if ($num > $count) {
$num = $count;
}
@@ -204,7 +207,6 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Append new elements to the list.
*
* @param array|Iterator $items Items to be appended. Existing keys will be overridden with the new values.
- *
* @return $this
*/
public function append($items)
@@ -228,10 +230,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
public function filter(callable $callback = null)
{
foreach ($this->items as $key => $value) {
- if (
- (!$callback && !(bool)$value) ||
- ($callback && !$callback($value, $key))
- ) {
+ if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) {
unset($this->items[$key]);
}
}
@@ -244,16 +243,13 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
* Sorts elements from the list and returns a copy of the list in the proper order
*
* @param callable|null $callback
- *
* @param bool $desc
- *
* @return $this|array
- * @internal param bool $asc
*
*/
public function sort(callable $callback = null, $desc = false)
{
- if (!$callback || !\is_callable($callback)) {
+ if (!$callback || !is_callable($callback)) {
return $this;
}
diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php
index a625146a..67097542 100644
--- a/system/src/Grav/Common/Language/Language.php
+++ b/system/src/Grav/Common/Language/Language.php
@@ -3,65 +3,96 @@
/**
* @package Grav\Common\Language
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Language;
+use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Config\Config;
use Negotiation\AcceptLanguage;
use Negotiation\LanguageNegotiator;
+use function array_key_exists;
+use function count;
+use function in_array;
+use function is_array;
+use function is_string;
+/**
+ * Class Language
+ * @package Grav\Common\Language
+ */
class Language
{
+ /** @var Grav */
protected $grav;
+ /** @var Config */
+ protected $config;
+ /** @var bool */
protected $enabled = true;
- /**
- * @var array
- */
+ /** @var array */
protected $languages = [];
- protected $page_extensions = [];
+ /** @var array */
protected $fallback_languages = [];
+ /** @var array */
+ protected $fallback_extensions = [];
+ /** @var array */
+ protected $page_extensions = [];
+ /** @var string|false */
protected $default;
- protected $active = null;
-
- /** @var Config $config */
- protected $config;
-
+ /** @var string|false */
+ protected $active;
+ /** @var array */
protected $http_accept_language;
+ /** @var bool */
protected $lang_in_url = false;
/**
* Constructor
*
- * @param \Grav\Common\Grav $grav
+ * @param Grav $grav
*/
public function __construct(Grav $grav)
{
$this->grav = $grav;
$this->config = $grav['config'];
- $this->languages = $this->config->get('system.languages.supported', []);
+
+ $languages = $this->config->get('system.languages.supported', []);
+ foreach ($languages as &$language) {
+ $language = (string)$language;
+ }
+ unset($language);
+
+ $this->languages = $languages;
+
$this->init();
}
/**
* Initialize the default and enabled languages
+ *
+ * @return void
*/
public function init()
{
$default = $this->config->get('system.languages.default_lang');
- if (isset($default) && $this->validate($default)) {
- $this->default = $default;
- } else {
- $this->default = reset($this->languages);
+ if (null !== $default) {
+ $default = (string)$default;
}
- $this->page_extensions = null;
+ // Note that reset returns false on empty languages.
+ $this->default = $default ?? reset($this->languages);
+
+ $this->resetFallbackPageExtensions();
if (empty($this->languages)) {
+ // If no languages are set, turn of multi-language support.
$this->enabled = false;
+ } elseif ($default && !in_array($default, $this->languages, true)) {
+ // If default language isn't in the language list, we need to add it.
+ array_unshift($this->languages, $default);
}
}
@@ -75,6 +106,16 @@ class Language
return $this->enabled;
}
+ /**
+ * Returns true if language debugging is turned on.
+ *
+ * @return bool
+ */
+ public function isDebug(): bool
+ {
+ return !$this->config->get('system.languages.translations', true);
+ }
+
/**
* Gets the array of supported languages
*
@@ -89,24 +130,27 @@ class Language
* Sets the current supported languages manually
*
* @param array $langs
+ * @return void
*/
public function setLanguages($langs)
{
$this->languages = $langs;
+
$this->init();
}
/**
* Gets a pipe-separated string of available languages
*
+ * @param string|null $delimiter Delimiter to be quoted.
* @return string
*/
- public function getAvailable()
+ public function getAvailable($delimiter = null)
{
$languagesArray = $this->languages; //Make local copy
- $languagesArray = array_map(function($value) {
- return preg_quote($value);
+ $languagesArray = array_map(static function ($value) use ($delimiter) {
+ return preg_quote($value, $delimiter);
}, $languagesArray);
sort($languagesArray);
@@ -117,7 +161,7 @@ class Language
/**
* Gets language, active if set, else default
*
- * @return string
+ * @return string|false
*/
public function getLanguage()
{
@@ -127,7 +171,7 @@ class Language
/**
* Gets current default language
*
- * @return mixed
+ * @return string|false
*/
public function getDefault()
{
@@ -138,11 +182,11 @@ class Language
* Sets default language manually
*
* @param string $lang
- *
- * @return bool
+ * @return string|bool
*/
public function setDefault($lang)
{
+ $lang = (string)$lang;
if ($this->validate($lang)) {
$this->default = $lang;
@@ -155,7 +199,7 @@ class Language
/**
* Gets current active language
*
- * @return string
+ * @return string|false
*/
public function getActive()
{
@@ -165,13 +209,17 @@ class Language
/**
* Sets active language manually
*
- * @param string $lang
- *
- * @return string|bool
+ * @param string|false $lang
+ * @return string|false
*/
public function setActive($lang)
{
+ $lang = (string)$lang;
if ($this->validate($lang)) {
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage('Active language set to ' . $lang, 'debug');
+
$this->active = $lang;
return $lang;
@@ -184,7 +232,6 @@ class Language
* Sets the active language based on the first part of the URL
*
* @param string $uri
- *
* @return string
*/
public function setActiveFromUri($uri)
@@ -196,7 +243,7 @@ class Language
// Try setting language from prefix of URL (/en/blah/blah).
if (preg_match($regex, $uri, $matches)) {
$this->lang_in_url = true;
- $this->active = $matches[2];
+ $this->setActive($matches[2]);
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
// Store in session if language is different.
@@ -210,22 +257,20 @@ class Language
// Try getting language from the session, else no active.
if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
$this->config->get('system.languages.session_store_active', true)) {
- $this->active = $this->grav['session']->active_language ?: null;
+ $this->setActive($this->grav['session']->active_language ?: null);
}
// if still null, try from http_accept_language header
if ($this->active === null &&
$this->config->get('system.languages.http_accept_language') &&
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) {
-
$negotiator = new LanguageNegotiator();
$best_language = $negotiator->getBest($accept, $this->languages);
if ($best_language instanceof AcceptLanguage) {
- $this->active = $best_language->getType();
+ $this->setActive($best_language->getType());
} else {
- $this->active = $this->getDefault();
+ $this->setActive($this->getDefault());
}
-
}
}
}
@@ -241,6 +286,10 @@ class Language
*/
public function getLanguageURLPrefix($lang = null)
{
+ if (!$this->enabled()) {
+ return '';
+ }
+
// if active lang is not passed in, use current active
if (!$lang) {
$lang = $this->getLanguage();
@@ -257,6 +306,10 @@ class Language
*/
public function isIncludeDefaultLanguage($lang = null)
{
+ if (!$this->enabled()) {
+ return false;
+ }
+
// if active lang is not passed in, use current active
if (!$lang) {
$lang = $this->getLanguage();
@@ -275,52 +328,63 @@ class Language
return (bool) $this->lang_in_url;
}
-
/**
- * Gets an array of valid extensions with active first, then fallback extensions
- *
- * @param string|null $file_ext
+ * Get full list of used language page extensions: [''=>'.md', 'en'=>'.en.md', ...]
*
+ * @param string|null $fileExtension
* @return array
*/
- public function getFallbackPageExtensions($file_ext = null)
+ public function getPageExtensions($fileExtension = null)
{
- if (empty($this->page_extensions)) {
- if (!$file_ext) {
- $file_ext = CONTENT_EXT;
+ $fileExtension = $fileExtension ?: CONTENT_EXT;
+
+ if (!isset($this->fallback_extensions[$fileExtension])) {
+ $extensions[''] = $fileExtension;
+ foreach ($this->languages as $code) {
+ $extensions[$code] = ".{$code}{$fileExtension}";
}
- if ($this->enabled()) {
- $valid_lang_extensions = [];
- foreach ($this->languages as $lang) {
- $valid_lang_extensions[] = '.' . $lang . $file_ext;
- }
+ $this->fallback_extensions[$fileExtension] = $extensions;
+ }
- if ($this->active) {
- $active_extension = '.' . $this->active . $file_ext;
- $key = \array_search($active_extension, $valid_lang_extensions, true);
+ return $this->fallback_extensions[$fileExtension];
+ }
- // Default behavior is to find any language other than active
- if ($this->config->get('system.languages.pages_fallback_only')) {
- $slice = \array_slice($valid_lang_extensions, 0, $key+1);
- $valid_lang_extensions = array_reverse($slice);
- } else {
- unset($valid_lang_extensions[$key]);
- array_unshift($valid_lang_extensions, $active_extension);
- }
+ /**
+ * Gets an array of valid extensions with active first, then fallback extensions
+ *
+ * @param string|null $fileExtension
+ * @param string|null $languageCode
+ * @param bool $assoc Return values in ['en' => '.en.md', ...] format.
+ * @return array Key is the language code, value is the file extension to be used.
+ */
+ public function getFallbackPageExtensions(string $fileExtension = null, string $languageCode = null, bool $assoc = false)
+ {
+ $fileExtension = $fileExtension ?: CONTENT_EXT;
+ $key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc;
+
+ if (!isset($this->fallback_extensions[$key])) {
+ $all = $this->getPageExtensions($fileExtension);
+ $list = [];
+ $fallback = $this->getFallbackLanguages($languageCode, true);
+ foreach ($fallback as $code) {
+ $ext = $all[$code] ?? null;
+ if (null !== $ext) {
+ $list[$code] = $ext;
}
- $valid_lang_extensions[] = $file_ext;
- $this->page_extensions = $valid_lang_extensions;
- } else {
- $this->page_extensions = (array)$file_ext;
}
+ if (!$assoc) {
+ $list = array_values($list);
+ }
+
+ $this->fallback_extensions[$key] = $list;
}
- return $this->page_extensions;
+ return $this->fallback_extensions[$key];
}
/**
- * Resets the page_extensions value.
+ * Resets the fallback_languages value.
*
* Useful to re-initialize the pages and change site language at runtime, example:
*
@@ -329,48 +393,90 @@ class Language
* $this->grav['language']->resetFallbackPageExtensions();
* $this->grav['pages']->init();
* ```
+ *
+ * @return void
*/
public function resetFallbackPageExtensions()
{
- $this->page_extensions = null;
+ $this->fallback_languages = [];
+ $this->fallback_extensions = [];
+ $this->page_extensions = [];
}
/**
- * Gets an array of languages with active first, then fallback languages
+ * Gets an array of languages with active first, then fallback languages.
+ *
*
+ * @param string|null $languageCode
+ * @param bool $includeDefault If true, list contains '', which can be used for default
* @return array
*/
- public function getFallbackLanguages()
+ public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false)
{
- if (empty($this->fallback_languages)) {
- if ($this->enabled()) {
- $fallback_languages = $this->languages;
-
- if ($this->active) {
- $active_extension = $this->active;
- $key = \array_search($active_extension, $fallback_languages, true);
- unset($fallback_languages[$key]);
- array_unshift($fallback_languages, $active_extension);
+ // Handle default.
+ if ($languageCode === '' || !$this->enabled()) {
+ return [''];
+ }
+
+ $default = $this->getDefault() ?? 'en';
+ $active = $languageCode ?? $this->getActive() ?? $default;
+ $key = $active . '-' . (int)$includeDefault;
+
+ if (!isset($this->fallback_languages[$key])) {
+ $fallback = $this->config->get('system.languages.content_fallback.' . $active);
+ $fallback_languages = [];
+
+ if (null === $fallback && $this->config->get('system.languages.pages_fallback_only', false)) {
+ user_error('Configuration option `system.languages.pages_fallback_only` is deprecated since Grav 1.7, use `system.languages.content_fallback` instead', E_USER_DEPRECATED);
+
+ // Special fallback list returns itself and all the previous items in reverse order:
+ // active: 'v2', languages: ['v1', 'v2', 'v3', 'v4'] => ['v2', 'v1', '']
+ if ($includeDefault) {
+ $fallback_languages[''] = '';
+ }
+ foreach ($this->languages as $code) {
+ $fallback_languages[$code] = $code;
+ if ($code === $active) {
+ break;
+ }
+ }
+ $fallback_languages = array_reverse($fallback_languages);
+ } else {
+ if (null === $fallback) {
+ $fallback = [$default];
+ } elseif (!is_array($fallback)) {
+ $fallback = is_string($fallback) && $fallback !== '' ? explode(',', $fallback) : [];
+ }
+ array_unshift($fallback, $active);
+ $fallback = array_unique($fallback);
+
+ foreach ($fallback as $code) {
+ // Default fallback list has active language followed by default language and extensionless file:
+ // active: 'fi', default: 'en', languages: ['sv', 'en', 'de', 'fi'] => ['fi', 'en', '']
+ $fallback_languages[$code] = $code;
+ if ($includeDefault && $code === $default) {
+ $fallback_languages[''] = '';
+ }
}
- $this->fallback_languages = $fallback_languages;
}
- // always add english in case a translation doesn't exist
- $this->fallback_languages[] = 'en';
+
+ $fallback_languages = array_values($fallback_languages);
+
+ $this->fallback_languages[$key] = $fallback_languages;
}
- return $this->fallback_languages;
+ return $this->fallback_languages[$key];
}
/**
* Ensures the language is valid and supported
*
* @param string $lang
- *
* @return bool
*/
public function validate($lang)
{
- return \in_array($lang, $this->languages, true);
+ return in_array($lang, $this->languages, true);
}
/**
@@ -378,45 +484,40 @@ class Language
*
* @param string|array $args The first argument is the lookup key value
* Other arguments can be passed and replaced in the translation with sprintf syntax
- * @param array $languages
+ * @param array|null $languages
* @param bool $array_support
* @param bool $html_out
- *
- * @return string
+ * @return string|string[]
*/
public function translate($args, array $languages = null, $array_support = false, $html_out = false)
{
- if (\is_array($args)) {
+ if (is_array($args)) {
$lookup = array_shift($args);
} else {
$lookup = $args;
$args = [];
}
- if ($this->config->get('system.languages.translations', true)) {
- if ($this->enabled() && $lookup) {
- if (empty($languages)) {
- if ($this->config->get('system.languages.translations_fallback', true)) {
- $languages = $this->getFallbackLanguages();
- } else {
- $languages = (array)$this->getLanguage();
- }
- }
- } else {
- $languages = ['en'];
+ if (!$this->isDebug()) {
+ if ($lookup && $this->enabled() && empty($languages)) {
+ $languages = $this->getTranslatedLanguages();
}
+ $languages = $languages ?: ['en'];
+
foreach ((array)$languages as $lang) {
$translation = $this->getTranslation($lang, $lookup, $array_support);
if ($translation) {
- if (\count($args) >= 1) {
+ if (is_string($translation) && count($args) >= 1) {
return vsprintf($translation, $args);
}
return $translation;
}
}
+ } elseif ($array_support) {
+ return [$lookup];
}
if ($html_out) {
@@ -433,29 +534,24 @@ class Language
* @param string $index
* @param array|null $languages
* @param bool $html_out
- *
* @return string
*/
public function translateArray($key, $index, $languages = null, $html_out = false)
{
- if ($this->config->get('system.languages.translations', true)) {
- if ($this->enabled() && $key) {
- if (empty($languages)) {
- if ($this->config->get('system.languages.translations_fallback', true)) {
- $languages = $this->getFallbackLanguages();
- } else {
- $languages = (array)$this->getDefault();
- }
- }
- } else {
- $languages = ['en'];
- }
+ if ($this->isDebug()) {
+ return $key . '[' . $index . ']';
+ }
- foreach ((array)$languages as $lang) {
- $translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
- if ($translation_array && array_key_exists($index, $translation_array)) {
- return $translation_array[$index];
- }
+ if ($key && empty($languages) && $this->enabled()) {
+ $languages = $this->getTranslatedLanguages();
+ }
+
+ $languages = $languages ?: ['en'];
+
+ foreach ((array)$languages as $lang) {
+ $translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
+ if ($translation_array && array_key_exists($index, $translation_array)) {
+ return $translation_array[$index];
}
}
@@ -472,11 +568,14 @@ class Language
* @param string $lang lang code
* @param string $key key to lookup with
* @param bool $array_support
- *
- * @return string
+ * @return string|string[]
*/
public function getTranslation($lang, $key, $array_support = false)
{
+ if ($this->isDebug()) {
+ return $key;
+ }
+
$translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
if (!$array_support && is_array($translation)) {
return (string)array_shift($translation);
@@ -527,11 +626,37 @@ class Language
*
* @param string $code
* @param string $type
- * @return bool
+ * @return string|false
*/
public function getLanguageCode($code, $type = 'name')
{
return LanguageCodes::get($code, $type);
}
+ /**
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ $vars = get_object_vars($this);
+ unset($vars['grav'], $vars['config']);
+
+ return $vars;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getTranslatedLanguages(): array
+ {
+ if ($this->config->get('system.languages.translations_fallback', true)) {
+ $languages = $this->getFallbackLanguages();
+ } else {
+ $languages = [$this->getLanguage()];
+ }
+
+ $languages[] = 'en';
+
+ return array_values(array_unique($languages));
+ }
}
diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php
index 780df7c0..f0a35322 100644
--- a/system/src/Grav/Common/Language/LanguageCodes.php
+++ b/system/src/Grav/Common/Language/LanguageCodes.php
@@ -3,14 +3,19 @@
/**
* @package Grav\Common\Language
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Language;
+/**
+ * Class LanguageCodes
+ * @package Grav\Common\Language
+ */
class LanguageCodes
{
+ /** @var array */
protected static $codes = [
'af' => [ 'name' => 'Afrikaans', 'nativeName' => 'Afrikaans' ],
'ak' => [ 'name' => 'Akan', 'nativeName' => 'Akan' ], // unverified native name
@@ -87,7 +92,7 @@ class LanguageCodes
'la' => [ 'name' => 'Latin', 'nativeName' => 'Latina' ],
'lb' => [ 'name' => 'Luxembourgish', 'nativeName' => 'Lëtzebuergesch' ],
'lg' => [ 'name' => 'Luganda', 'nativeName' => 'Luganda' ],
- 'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių kalba' ],
+ 'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių' ],
'lv' => [ 'name' => 'Latvian', 'nativeName' => 'Latviešu' ],
'mai' => [ 'name' => 'Maithili', 'nativeName' => 'मैथिली মৈথিলী' ],
'mg' => [ 'name' => 'Malagasy', 'nativeName' => 'Malagasy' ],
@@ -150,11 +155,19 @@ class LanguageCodes
'zu' => [ 'name' => 'Zulu', 'nativeName' => 'isiZulu' ]
];
+ /**
+ * @param string $code
+ * @return string|false
+ */
public static function getName($code)
{
return static::get($code, 'name');
}
+ /**
+ * @param string $code
+ * @return string|false
+ */
public static function getNativeName($code)
{
if (isset(static::$codes[$code])) {
@@ -168,21 +181,28 @@ class LanguageCodes
return $code;
}
+ /**
+ * @param string $code
+ * @return string
+ */
public static function getOrientation($code)
{
- if (isset(static::$codes[$code])) {
- if (isset(static::$codes[$code]['orientation'])) {
- return static::get($code, 'orientation');
- }
- }
- return 'ltr';
+ return static::$codes[$code]['orientation'] ?? 'ltr';
}
+ /**
+ * @param string $code
+ * @return bool
+ */
public static function isRtl($code)
{
return static::getOrientation($code) === 'rtl';
}
+ /**
+ * @param array $keys
+ * @return array
+ */
public static function getNames(array $keys)
{
$results = [];
@@ -194,12 +214,27 @@ class LanguageCodes
return $results;
}
+ /**
+ * @param string $code
+ * @param string $type
+ * @return string|false
+ */
public static function get($code, $type)
{
- if (isset(static::$codes[$code][$type])) {
- return static::$codes[$code][$type];
+ return static::$codes[$code][$type] ?? false;
+ }
+
+ /**
+ * @param bool $native
+ * @return array
+ */
+ public static function getList($native = true)
+ {
+ $list = [];
+ foreach (static::$codes as $key => $names) {
+ $list[$key] = $native ? $names['nativeName'] : $names['name'];
}
- return false;
+ return $list;
}
}
diff --git a/system/src/Grav/Common/Markdown/Parsedown.php b/system/src/Grav/Common/Markdown/Parsedown.php
index 8b8565ff..bdc5bdcd 100644
--- a/system/src/Grav/Common/Markdown/Parsedown.php
+++ b/system/src/Grav/Common/Markdown/Parsedown.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Markdown
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,16 +11,19 @@ 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 ParsedownLib
+/**
+ * Class Parsedown
+ * @package Grav\Common\Markdown
+ */
+class Parsedown extends \Parsedown
{
use ParsedownGravTrait;
/**
* Parsedown constructor.
*
- * @param Excerpts|null $excerpts
+ * @param Excerpts|PageInterface|null $excerpts
* @param array|null $defaults
*/
public function __construct($excerpts = null, $defaults = null)
@@ -36,5 +39,4 @@ class Parsedown extends ParsedownLib
$this->init($excerpts, $defaults);
}
-
}
diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php
index 70c85961..b8e760ef 100644
--- a/system/src/Grav/Common/Markdown/ParsedownExtra.php
+++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php
@@ -3,26 +3,30 @@
/**
* @package Grav\Common\Markdown
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Markdown;
+use Exception;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
-use Grav\Framework\Parsedown\ParsedownExtra as ParsedownExtraLib;
-class ParsedownExtra extends ParsedownExtraLib
+/**
+ * Class ParsedownExtra
+ * @package Grav\Common\Markdown
+ */
+class ParsedownExtra extends \ParsedownExtra
{
use ParsedownGravTrait;
/**
* ParsedownExtra constructor.
*
- * @param Excerpts|null $excerpts
+ * @param Excerpts|PageInterface|null $excerpts
* @param array|null $defaults
- * @throws \Exception
+ * @throws Exception
*/
public function __construct($excerpts = null, $defaults = null)
{
diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
index aa76cdf0..7c2b0d66 100644
--- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
+++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Markdown
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,23 +11,34 @@ namespace Grav\Common\Markdown;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Page\Interfaces\PageInterface;
+use function call_user_func_array;
+use function in_array;
+use function strlen;
+/**
+ * Trait ParsedownGravTrait
+ * @package Grav\Common\Markdown
+ */
trait ParsedownGravTrait
{
+ /** @var array */
+ public $completable_blocks = [];
+ /** @var array */
+ public $continuable_blocks = [];
+
/** @var Excerpts */
protected $excerpts;
-
+ /** @var array */
protected $special_chars;
+ /** @var string */
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
- public $completable_blocks = [];
- public $continuable_blocks = [];
-
/**
* Initialization function to setup key variables needed by the MarkdownGravLinkTrait
*
* @param PageInterface|Excerpts|null $excerpts
* @param array|null $defaults
+ * @return void
*/
protected function init($excerpts = null, $defaults = null)
{
@@ -79,6 +90,7 @@ trait ParsedownGravTrait
* @param bool $continuable
* @param bool $completable
* @param int|null $index
+ * @return void
*/
public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null)
{
@@ -110,6 +122,7 @@ trait ParsedownGravTrait
* @param string $type
* @param string $tag
* @param int|null $index
+ * @return void
*/
public function addInlineType($type, $tag, $index = null)
{
@@ -128,12 +141,11 @@ trait ParsedownGravTrait
* Overrides the default behavior to allow for plugin-provided blocks to be continuable
*
* @param string $Type
- *
* @return bool
*/
protected function isBlockContinuable($Type)
{
- $continuable = \in_array($Type, $this->continuable_blocks, true)
+ $continuable = in_array($Type, $this->continuable_blocks, true)
|| method_exists($this, 'block' . $Type . 'Continue');
return $continuable;
@@ -143,12 +155,11 @@ trait ParsedownGravTrait
* Overrides the default behavior to allow for plugin-provided blocks to be completable
*
* @param string $Type
- *
* @return bool
*/
protected function isBlockCompletable($Type)
{
- $completable = \in_array($Type, $this->completable_blocks, true)
+ $completable = in_array($Type, $this->completable_blocks, true)
|| method_exists($this, 'block' . $Type . 'Complete');
return $completable;
@@ -159,7 +170,6 @@ trait ParsedownGravTrait
* Make the element function publicly accessible, Medium uses this to render from Twig
*
* @param array $Element
- *
* @return string markup
*/
public function elementToHtml(array $Element)
@@ -171,7 +181,6 @@ trait ParsedownGravTrait
* Setter for special chars
*
* @param array $special_chars
- *
* @return $this
*/
public function setSpecialChars($special_chars)
@@ -196,6 +205,10 @@ trait ParsedownGravTrait
return null;
}
+ /**
+ * @param array $excerpt
+ * @return array|null
+ */
protected function inlineSpecialCharacter($excerpt)
{
if ($excerpt['text'][0] === '&' && !preg_match('/^?\w+;/', $excerpt['text'])) {
@@ -215,6 +228,10 @@ trait ParsedownGravTrait
return null;
}
+ /**
+ * @param array $excerpt
+ * @return array
+ */
protected function inlineImage($excerpt)
{
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
@@ -237,6 +254,10 @@ trait ParsedownGravTrait
return $excerpt;
}
+ /**
+ * @param array $excerpt
+ * @return array
+ */
protected function inlineLink($excerpt)
{
$type = $excerpt['type'] ?? 'link';
@@ -263,13 +284,17 @@ trait ParsedownGravTrait
/**
* For extending this class via plugins
+ *
+ * @param string $method
+ * @param array $args
+ * @return mixed|null
*/
public function __call($method, $args)
{
if (isset($this->{$method}) === true) {
$func = $this->{$method};
- return \call_user_func_array($func, $args);
+ return call_user_func_array($func, $args);
}
return null;
diff --git a/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php
new file mode 100644
index 00000000..d9c69d28
--- /dev/null
+++ b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php
@@ -0,0 +1,25 @@
+get('this.is.my.nested.variable');
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $default Default value (or null).
+ * @param string|null $separator Separator, defaults to '.'
+ * @return mixed Value.
+ */
+ public function get($name, $default = null, $separator = null);
+
+ /**
+ * Set value by using dot notation for nested arrays/objects.
+ *
+ * @example $data->set('this.is.my.nested.variable', $value);
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $value New value.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ public function set($name, $value, $separator = null);
}
diff --git a/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php
new file mode 100644
index 00000000..9307915c
--- /dev/null
+++ b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php
@@ -0,0 +1,56 @@
+ 'user://pages/media']; // Settings from the form field.
+ * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
+ * $media->copyUploadedFile($uploadedFile, $filename);
+
+ * @param UploadedFileInterface $uploadedFile
+ * @param string|null $filename
+ * @param array|null $settings
+ * @return string
+ * @throws RuntimeException
+ */
+ public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string;
+
+ /**
+ * Copy uploaded file to the media collection.
+ *
+ * WARNING: Always check uploaded file before copying it!
+ *
+ * @example
+ * $filename = null; // Override filename if needed (ignored if randomizing filenames).
+ * $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
+ * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
+ * $media->copyUploadedFile($uploadedFile, $filename);
+ *
+ * @param UploadedFileInterface $uploadedFile
+ * @param string $filename
+ * @param array|null $settings
+ * @return void
+ * @throws RuntimeException
+ */
+ public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename, array $settings = null): void;
+
+ /**
+ * Delete real file from the media collection.
+ *
+ * @param string $filename
+ * @param array|null $settings
+ * @return void
+ */
+ public function deleteFile(string $filename, array $settings = null): void;
+
+ /**
+ * Rename file inside the media collection.
+ *
+ * @param string $from
+ * @param string $to
+ * @param array|null $settings
+ */
+ public function renameFile(string $from, string $to, array $settings = null): void;
+}
diff --git a/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php b/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php
new file mode 100644
index 00000000..ff0655a6
--- /dev/null
+++ b/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php
@@ -0,0 +1,32 @@
+attributes['controlsList'] = $controlsList;
+
+ return $this;
+ }
+
+ /**
+ * Parsedown element for source display mode
+ *
+ * @param array $attributes
+ * @param bool $reset
+ * @return array
+ */
+ protected function sourceParsedownElement(array $attributes, $reset = true)
+ {
+ $location = $this->url($reset);
+
+ return [
+ 'name' => 'audio',
+ 'rawHtml' => 'Your browser does not support the audio tag.',
+ 'attributes' => $attributes
+ ];
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php b/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php
new file mode 100644
index 00000000..c0237638
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php
@@ -0,0 +1,37 @@
+get('system.images.defaults.loading', 'auto');
+ }
+ if ($value && $value !== 'auto') {
+ $this->attributes['loading'] = $value;
+ }
+
+ return $this;
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php
new file mode 100644
index 00000000..06056ebc
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php
@@ -0,0 +1,420 @@
+ [0, 1],
+ 'forceResize' => [0, 1],
+ 'cropResize' => [0, 1],
+ 'crop' => [0, 1, 2, 3],
+ 'zoomCrop' => [0, 1]
+ ];
+
+ /** @var string */
+ protected $sizes = '100vw';
+
+
+ /**
+ * Allows the ability to override the image's pretty name stored in cache
+ *
+ * @param string $name
+ */
+ public function setImagePrettyName($name)
+ {
+ $this->set('prettyname', $name);
+ if ($this->image) {
+ $this->image->setPrettyName($name);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getImagePrettyName()
+ {
+ if ($this->get('prettyname')) {
+ return $this->get('prettyname');
+ }
+
+ $basename = $this->get('basename');
+ if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
+ $basename = $matches[1];
+ }
+ return $basename;
+ }
+
+ /**
+ * Simply processes with no extra methods. Useful for triggering events.
+ *
+ * @return $this
+ */
+ public function cache()
+ {
+ if (!$this->image) {
+ $this->image();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Generate alternative image widths, using either an array of integers, or
+ * a min width, a max width, and a step parameter to fill out the necessary
+ * widths. Existing image alternatives won't be overwritten.
+ *
+ * @param int|int[] $min_width
+ * @param int $max_width
+ * @param int $step
+ * @return $this
+ */
+ public function derivatives($min_width, $max_width = 2500, $step = 200)
+ {
+ if (!empty($this->alternatives)) {
+ $max = max(array_keys($this->alternatives));
+ $base = $this->alternatives[$max];
+ } else {
+ $base = $this;
+ }
+
+ $widths = [];
+
+ if (func_num_args() === 1) {
+ foreach ((array) func_get_arg(0) as $width) {
+ if ($width < $base->get('width')) {
+ $widths[] = $width;
+ }
+ }
+ } else {
+ $max_width = min($max_width, $base->get('width'));
+
+ for ($width = $min_width; $width < $max_width; $width += $step) {
+ $widths[] = $width;
+ }
+ }
+
+ foreach ($widths as $width) {
+ // Only generate image alternatives that don't already exist
+ if (array_key_exists((int) $width, $this->alternatives)) {
+ continue;
+ }
+
+ $derivative = MediumFactory::fromFile($base->get('filepath'));
+
+ // It's possible that MediumFactory::fromFile returns null if the
+ // original image file no longer exists and this class instance was
+ // retrieved from the page cache
+ if (null !== $derivative) {
+ $index = 2;
+ $alt_widths = array_keys($this->alternatives);
+ sort($alt_widths);
+
+ foreach ($alt_widths as $i => $key) {
+ if ($width > $key) {
+ $index += max($i, 1);
+ }
+ }
+
+ $basename = preg_replace('/(@\d+x)?$/', "@{$width}w", $base->get('basename'), 1);
+ $derivative->setImagePrettyName($basename);
+
+ $ratio = $base->get('width') / $width;
+ $height = $derivative->get('height') / $ratio;
+
+ $derivative->resize($width, $height);
+ $derivative->set('width', $width);
+ $derivative->set('height', $height);
+
+ $this->addAlternative($ratio, $derivative);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Clear out the alternatives.
+ */
+ public function clearAlternatives()
+ {
+ $this->alternatives = [];
+ }
+
+ /**
+ * Sets or gets the quality of the image
+ *
+ * @param int|null $quality 0-100 quality
+ * @return int|$this
+ */
+ public function quality($quality = null)
+ {
+ if ($quality) {
+ if (!$this->image) {
+ $this->image();
+ }
+
+ $this->quality = $quality;
+
+ return $this;
+ }
+
+ return $this->quality;
+ }
+
+ /**
+ * Sets image output format.
+ *
+ * @param string $format
+ * @return $this
+ */
+ public function format($format)
+ {
+ if (!$this->image) {
+ $this->image();
+ }
+
+ $this->format = $format;
+
+ return $this;
+ }
+
+ /**
+ * Set or get sizes parameter for srcset media action
+ *
+ * @param string|null $sizes
+ * @return string
+ */
+ public function sizes($sizes = null)
+ {
+ if ($sizes) {
+ $this->sizes = $sizes;
+
+ return $this;
+ }
+
+ return empty($this->sizes) ? '100vw' : $this->sizes;
+ }
+
+ /**
+ * Allows to set the width attribute from Markdown or Twig
+ * Examples: 
+ * 
+ * 
+ * 
+ * {{ page.media['myimg.png'].width().height().html }}
+ * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
+ *
+ * @param string|int $value A value or 'auto' or empty to use the width of the image
+ * @return $this
+ */
+ public function width($value = 'auto')
+ {
+ if (!$value || $value === 'auto') {
+ $this->attributes['width'] = $this->get('width');
+ } else {
+ $this->attributes['width'] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the height attribute from Markdown or Twig
+ * Examples: 
+ * 
+ * 
+ * 
+ * {{ page.media['myimg.png'].width().height().html }}
+ * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
+ *
+ * @param string|int $value A value or 'auto' or empty to use the height of the image
+ * @return $this
+ */
+ public function height($value = 'auto')
+ {
+ if (!$value || $value === 'auto') {
+ $this->attributes['height'] = $this->get('height');
+ } else {
+ $this->attributes['height'] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter image by using user defined filter parameters.
+ *
+ * @param string $filter Filter to be used.
+ * @return $this
+ */
+ public function filter($filter = 'image.filters.default')
+ {
+ $filters = (array) $this->get($filter, []);
+ foreach ($filters as $params) {
+ $params = (array) $params;
+ $method = array_shift($params);
+ $this->__call($method, $params);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the image higher quality version
+ *
+ * @return ImageMediaInterface|$this the alternative version with higher quality
+ */
+ public function higherQualityAlternative()
+ {
+ if ($this->alternatives) {
+ /** @var ImageMedium $max */
+ $max = reset($this->alternatives);
+ /** @var ImageMedium $alternative */
+ foreach ($this->alternatives as $alternative) {
+ if ($alternative->quality() > $max->quality()) {
+ $max = $alternative;
+ }
+ }
+
+ return $max;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets medium image, resets image manipulation operations.
+ *
+ * @return $this
+ */
+ protected function image()
+ {
+ $locator = Grav::instance()['locator'];
+
+ // Use existing cache folder or if it doesn't exist, create it.
+ $cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
+
+ // Make sure we free previous image.
+ unset($this->image);
+
+ /** @var MediaCollectionInterface $media */
+ $media = $this->get('media');
+ if ($media && method_exists($media, 'getImageFileObject')) {
+ $this->image = $media->getImageFileObject($this);
+ } else {
+ $this->image = ImageFile::open($this->get('filepath'));
+ }
+
+ $this->image
+ ->setCacheDir($cacheDir)
+ ->setActualCacheDir($cacheDir)
+ ->setPrettyName($this->getImagePrettyName());
+
+ // Fix orientation if enabled
+ $config = Grav::instance()['config'];
+ if ($config->get('system.images.auto_fix_orientation', false) &&
+ extension_loaded('exif') && function_exists('exif_read_data')) {
+ $this->image->fixOrientation();
+ }
+
+ // Set CLS configuration
+ $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false);
+ $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false);
+ $this->retina_scale = $config->get('system.images.cls.retina_scale', 1);
+
+ return $this;
+ }
+
+ /**
+ * Save the image with cache.
+ *
+ * @return string
+ */
+ protected function saveImage()
+ {
+ if (!$this->image) {
+ return parent::path(false);
+ }
+
+ $this->filter();
+
+ if (isset($this->result)) {
+ return $this->result;
+ }
+
+ if ($this->format === 'guess') {
+ $extension = strtolower($this->get('extension'));
+ $this->format($extension);
+ }
+
+ if (!$this->debug_watermarked && $this->get('debug')) {
+ $ratio = $this->get('ratio');
+ if (!$ratio) {
+ $ratio = 1;
+ }
+
+ $locator = Grav::instance()['locator'];
+ $overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
+ $this->image->merge(ImageFile::open($overlay));
+ }
+
+ return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/MediaFileTrait.php b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php
new file mode 100644
index 00000000..71981331
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php
@@ -0,0 +1,139 @@
+path(false);
+
+ return file_exists($path);
+ }
+
+ /**
+ * Get file modification time for the medium.
+ *
+ * @return int|null
+ */
+ public function modified()
+ {
+ $path = $this->path(false);
+ if (!file_exists($path)) {
+ return null;
+ }
+
+ return filemtime($path) ?: null;
+ }
+
+ /**
+ * Get size of the medium.
+ *
+ * @return int
+ */
+ public function size()
+ {
+ $path = $this->path(false);
+ if (!file_exists($path)) {
+ return 0;
+ }
+
+ return filesize($path) ?: 0;
+ }
+
+ /**
+ * Return PATH to file.
+ *
+ * @param bool $reset
+ * @return string path to file
+ */
+ public function path($reset = true)
+ {
+ if ($reset) {
+ $this->reset();
+ }
+
+ return $this->get('url') ?? $this->get('filepath');
+ }
+
+ /**
+ * Return the relative path to file
+ *
+ * @param bool $reset
+ * @return string
+ */
+ public function relativePath($reset = true)
+ {
+ if ($reset) {
+ $this->reset();
+ }
+
+ $path = $this->path(false);
+ $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $path) ?: $path;
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+ if ($locator->isStream($output)) {
+ $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true));
+ }
+
+ return $output;
+ }
+
+ /**
+ * Return URL to file.
+ *
+ * @param bool $reset
+ * @return string
+ */
+ public function url($reset = true)
+ {
+ $url = $this->get('url');
+ if ($url) {
+ return $url;
+ }
+
+ $path = $this->relativePath($reset);
+
+ return trim($this->getGrav()['base_url'] . '/' . $this->urlQuerystring($path), '\\');
+ }
+
+ /**
+ * Get the URL with full querystring
+ *
+ * @param string $url
+ * @return string
+ */
+ abstract public function urlQuerystring($url);
+
+ /**
+ * Reset medium.
+ *
+ * @return $this
+ */
+ abstract public function reset();
+
+ /**
+ * @return Grav
+ */
+ abstract protected function getGrav(): Grav;
+}
diff --git a/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php
new file mode 100644
index 00000000..3c2a38ba
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php
@@ -0,0 +1,609 @@
+getItems());
+ }
+
+ /**
+ * Set querystring to file modification timestamp (or value provided as a parameter).
+ *
+ * @param string|int|null $timestamp
+ * @return $this
+ */
+ public function setTimestamp($timestamp = null)
+ {
+ if (null !== $timestamp) {
+ $this->timestamp = (string)($timestamp);
+ } elseif ($this instanceof MediaFileInterface) {
+ $this->timestamp = (string)$this->modified();
+ } else {
+ $this->timestamp = '';
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns an array containing just the metadata
+ *
+ * @return array
+ */
+ public function metadata()
+ {
+ return $this->metadata;
+ }
+
+ /**
+ * Add meta file for the medium.
+ *
+ * @param string $filepath
+ */
+ abstract public function addMetaFile($filepath);
+
+ /**
+ * Add alternative Medium to this Medium.
+ *
+ * @param int|float $ratio
+ * @param MediaObjectInterface $alternative
+ */
+ public function addAlternative($ratio, MediaObjectInterface $alternative)
+ {
+ if (!is_numeric($ratio) || $ratio === 0) {
+ return;
+ }
+
+ $alternative->set('ratio', $ratio);
+ $width = $alternative->get('width');
+
+ $this->alternatives[$width] = $alternative;
+ }
+
+ /**
+ * Return string representation of the object (html).
+ *
+ * @return string
+ */
+ abstract public function __toString();
+
+ /**
+ * Get/set querystring for the file's url
+ *
+ * @param string|null $querystring
+ * @param bool $withQuestionmark
+ * @return string
+ */
+ public function querystring($querystring = null, $withQuestionmark = true)
+ {
+ if (null !== $querystring) {
+ $this->medium_querystring[] = ltrim($querystring, '?&');
+ foreach ($this->alternatives as $alt) {
+ $alt->querystring($querystring, $withQuestionmark);
+ }
+ }
+
+ if (empty($this->medium_querystring)) {
+ return '';
+ }
+
+ // join the strings
+ $querystring = implode('&', $this->medium_querystring);
+ // explode all strings
+ $query_parts = explode('&', $querystring);
+ // Join them again now ensure the elements are unique
+ $querystring = implode('&', array_unique($query_parts));
+
+ return $withQuestionmark ? ('?' . $querystring) : $querystring;
+ }
+
+ /**
+ * Get the URL with full querystring
+ *
+ * @param string $url
+ * @return string
+ */
+ public function urlQuerystring($url)
+ {
+ $querystring = $this->querystring();
+ if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) {
+ $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp);
+ }
+
+ return ltrim($url . $querystring . $this->urlHash(), '/');
+ }
+
+ /**
+ * Get/set hash for the file's url
+ *
+ * @param string|null $hash
+ * @param bool $withHash
+ * @return string
+ */
+ public function urlHash($hash = null, $withHash = true)
+ {
+ if ($hash) {
+ $this->set('urlHash', ltrim($hash, '#'));
+ }
+
+ $hash = $this->get('urlHash', '');
+
+ return $withHash && !empty($hash) ? '#' . $hash : $hash;
+ }
+
+ /**
+ * Get an element (is array) that can be rendered by the Parsedown engine
+ *
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
+ * @param bool $reset
+ * @return array
+ */
+ public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
+ {
+ $attributes = $this->attributes;
+ $items = $this->getItems();
+
+ $style = '';
+ foreach ($this->styleAttributes as $key => $value) {
+ if (is_numeric($key)) { // Special case for inline style attributes, refer to style() method
+ $style .= $value;
+ } else {
+ $style .= $key . ': ' . $value . ';';
+ }
+ }
+ if ($style) {
+ $attributes['style'] = $style;
+ }
+
+ if (empty($attributes['title'])) {
+ if (!empty($title)) {
+ $attributes['title'] = $title;
+ } elseif (!empty($items['title'])) {
+ $attributes['title'] = $items['title'];
+ }
+ }
+
+ if (empty($attributes['alt'])) {
+ if (!empty($alt)) {
+ $attributes['alt'] = $alt;
+ } elseif (!empty($items['alt'])) {
+ $attributes['alt'] = $items['alt'];
+ } elseif (!empty($items['alt_text'])) {
+ $attributes['alt'] = $items['alt_text'];
+ } else {
+ $attributes['alt'] = '';
+ }
+ }
+
+ if (empty($attributes['class'])) {
+ if (!empty($class)) {
+ $attributes['class'] = $class;
+ } elseif (!empty($items['class'])) {
+ $attributes['class'] = $items['class'];
+ }
+ }
+
+ if (empty($attributes['id'])) {
+ if (!empty($id)) {
+ $attributes['id'] = $id;
+ } elseif (!empty($items['id'])) {
+ $attributes['id'] = $items['id'];
+ }
+ }
+
+ switch ($this->mode) {
+ case 'text':
+ $element = $this->textParsedownElement($attributes, false);
+ break;
+ case 'thumbnail':
+ $thumbnail = $this->getThumbnail();
+ $element = $thumbnail ? $thumbnail->sourceParsedownElement($attributes, false) : [];
+ break;
+ case 'source':
+ $element = $this->sourceParsedownElement($attributes, false);
+ break;
+ default:
+ $element = [];
+ }
+
+ if ($reset) {
+ $this->reset();
+ }
+
+ $this->display('source');
+
+ return $element;
+ }
+
+ /**
+ * Reset medium.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->attributes = [];
+
+ return $this;
+ }
+
+ /**
+ * Add custom attribute to medium.
+ *
+ * @param string $attribute
+ * @param string $value
+ * @return $this
+ */
+ public function attribute($attribute = null, $value = '')
+ {
+ if (!empty($attribute)) {
+ $this->attributes[$attribute] = $value;
+ }
+ return $this;
+ }
+
+ /**
+ * Switch display mode.
+ *
+ * @param string $mode
+ *
+ * @return MediaObjectInterface|null
+ */
+ public function display($mode = 'source')
+ {
+ if ($this->mode === $mode) {
+ return $this;
+ }
+
+ $this->mode = $mode;
+ if ($mode === 'thumbnail') {
+ $thumbnail = $this->getThumbnail();
+
+ return $thumbnail ? $thumbnail->reset() : null;
+ }
+
+ return $this->reset();
+ }
+
+ /**
+ * Helper method to determine if this media item has a thumbnail or not
+ *
+ * @param string $type;
+ * @return bool
+ */
+ public function thumbnailExists($type = 'page')
+ {
+ $thumbs = $this->get('thumbnails');
+
+ return isset($thumbs[$type]);
+ }
+
+ /**
+ * Switch thumbnail.
+ *
+ * @param string $type
+ * @return $this
+ */
+ public function thumbnail($type = 'auto')
+ {
+ if ($type !== 'auto' && !in_array($type, $this->thumbnailTypes, true)) {
+ return $this;
+ }
+
+ if ($this->thumbnailType !== $type) {
+ $this->_thumbnail = null;
+ }
+
+ $this->thumbnailType = $type;
+
+ return $this;
+ }
+
+ /**
+ * Return URL to file.
+ *
+ * @param bool $reset
+ * @return string
+ */
+ abstract public function url($reset = true);
+
+ /**
+ * Turn the current Medium into a Link
+ *
+ * @param bool $reset
+ * @param array $attributes
+ * @return MediaLinkInterface
+ */
+ public function link($reset = true, array $attributes = [])
+ {
+ if ($this->mode !== 'source') {
+ $this->display('source');
+ }
+
+ foreach ($this->attributes as $key => $value) {
+ empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value;
+ }
+
+ empty($attributes['href']) && $attributes['href'] = $this->url();
+
+ return $this->createLink($attributes);
+ }
+
+ /**
+ * Turn the current Medium into a Link with lightbox enabled
+ *
+ * @param int|null $width
+ * @param int|null $height
+ * @param bool $reset
+ * @return MediaLinkInterface
+ */
+ public function lightbox($width = null, $height = null, $reset = true)
+ {
+ $attributes = ['rel' => 'lightbox'];
+
+ if ($width && $height) {
+ $attributes['data-width'] = $width;
+ $attributes['data-height'] = $height;
+ }
+
+ return $this->link($reset, $attributes);
+ }
+
+ /**
+ * Add a class to the element from Markdown or Twig
+ * Example:  or 
+ *
+ * @return $this
+ */
+ public function classes()
+ {
+ $classes = func_get_args();
+ if (!empty($classes)) {
+ $this->attributes['class'] = implode(',', $classes);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add an id to the element from Markdown or Twig
+ * Example: 
+ *
+ * @param string $id
+ * @return $this
+ */
+ public function id($id)
+ {
+ if (is_string($id)) {
+ $this->attributes['id'] = trim($id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to add an inline style attribute from Markdown or Twig
+ * Example: 
+ *
+ * @param string $style
+ * @return $this
+ */
+ public function style($style)
+ {
+ $this->styleAttributes[] = rtrim($style, ';') . ';';
+
+ return $this;
+ }
+
+ /**
+ * Allow any action to be called on this medium from twig or markdown
+ *
+ * @param string $method
+ * @param array $args
+ * @return $this
+ */
+ public function __call($method, $args)
+ {
+ $count = count($args);
+ if ($count > 1 || ($count === 1 && !empty($args[0]))) {
+ $method .= '=' . implode(',', array_map(static function ($a) {
+ if (is_array($a)) {
+ $a = '[' . implode(',', $a) . ']';
+ }
+
+ return rawurlencode($a);
+ }, $args));
+ }
+
+ if (!empty($method)) {
+ $this->querystring($this->querystring(null, false) . '&' . $method);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Parsedown element for source display mode
+ *
+ * @param array $attributes
+ * @param bool $reset
+ * @return array
+ */
+ protected function sourceParsedownElement(array $attributes, $reset = true)
+ {
+ return $this->textParsedownElement($attributes, $reset);
+ }
+
+ /**
+ * Parsedown element for text display mode
+ *
+ * @param array $attributes
+ * @param bool $reset
+ * @return array
+ */
+ protected function textParsedownElement(array $attributes, $reset = true)
+ {
+ if ($reset) {
+ $this->reset();
+ }
+
+ $text = $attributes['title'] ?? '';
+ if ($text === '') {
+ $text = $attributes['alt'] ?? '';
+ if ($text === '') {
+ $text = $this->get('filename');
+ }
+ }
+
+ return [
+ 'name' => 'p',
+ 'attributes' => $attributes,
+ 'text' => $text
+ ];
+ }
+
+ /**
+ * Get the thumbnail Medium object
+ *
+ * @return ThumbnailImageMedium|null
+ */
+ protected function getThumbnail()
+ {
+ if (null === $this->_thumbnail) {
+ $types = $this->thumbnailTypes;
+
+ if ($this->thumbnailType !== 'auto') {
+ array_unshift($types, $this->thumbnailType);
+ }
+
+ foreach ($types as $type) {
+ $thumb = $this->get("thumbnails.{$type}", false);
+
+ if ($thumb) {
+ $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : $this->createThumbnail($thumb);
+ $thumb->parent = $this;
+ $this->_thumbnail = $thumb;
+ break;
+ }
+ }
+ }
+
+ return $this->_thumbnail;
+ }
+
+ /**
+ * Get value by using dot notation for nested arrays/objects.
+ *
+ * @example $value = $this->get('this.is.my.nested.variable');
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $default Default value (or null).
+ * @param string|null $separator Separator, defaults to '.'
+ * @return mixed Value.
+ */
+ abstract public function get($name, $default = null, $separator = null);
+
+ /**
+ * Set value by using dot notation for nested arrays/objects.
+ *
+ * @example $data->set('this.is.my.nested.variable', $value);
+ *
+ * @param string $name Dot separated path to the requested value.
+ * @param mixed $value New value.
+ * @param string|null $separator Separator, defaults to '.'
+ * @return $this
+ */
+ abstract public function set($name, $value, $separator = null);
+
+ /**
+ * @param string $thumb
+ */
+ abstract protected function createThumbnail($thumb);
+
+ /**
+ * @param array $attributes
+ * @return MediaLinkInterface
+ */
+ abstract protected function createLink(array $attributes);
+
+ /**
+ * @return array
+ */
+ abstract protected function getItems(): array;
+}
diff --git a/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php
new file mode 100644
index 00000000..66e9f47d
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php
@@ -0,0 +1,113 @@
+attributes['controls'] = true;
+ } else {
+ unset($this->attributes['controls']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the loop attribute
+ *
+ * @param bool $status
+ * @return $this
+ */
+ public function loop($status = false)
+ {
+ if ($status) {
+ $this->attributes['loop'] = true;
+ } else {
+ unset($this->attributes['loop']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the autoplay attribute
+ *
+ * @param bool $status
+ * @return $this
+ */
+ public function autoplay($status = false)
+ {
+ if ($status) {
+ $this->attributes['autoplay'] = true;
+ } else {
+ unset($this->attributes['autoplay']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the muted attribute
+ *
+ * @param bool $status
+ * @return $this
+ */
+ public function muted($status = false)
+ {
+ if ($status) {
+ $this->attributes['muted'] = true;
+ } else {
+ unset($this->attributes['muted']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the preload behaviour
+ *
+ * @param string|null $preload
+ * @return $this
+ */
+ public function preload($preload = null)
+ {
+ $validPreloadAttrs = ['auto', 'metadata', 'none'];
+
+ if (null === $preload) {
+ unset($this->attributes['preload']);
+ } elseif (in_array($preload, $validPreloadAttrs, true)) {
+ $this->attributes['preload'] = $preload;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Reset player.
+ */
+ public function resetPlayer()
+ {
+ $this->attributes['controls'] = true;
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/MediaTrait.php b/system/src/Grav/Common/Media/Traits/MediaTrait.php
index dc6550db..5faba826 100644
--- a/system/src/Grav/Common/Media/Traits/MediaTrait.php
+++ b/system/src/Grav/Common/Media/Traits/MediaTrait.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Media
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,10 +15,18 @@ use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media;
use Psr\SimpleCache\CacheInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function strlen;
+/**
+ * Trait MediaTrait
+ * @package Grav\Common\Media\Traits
+ */
trait MediaTrait
{
+ /** @var MediaCollectionInterface|null */
protected $media;
+ /** @var bool */
+ protected $_loadMedia = true;
/**
* Get filesystem path to the associated media.
@@ -40,52 +48,58 @@ trait MediaTrait
/**
* Get URI ot the associated media. Method will return null if path isn't URI.
*
- * @return null|string
+ * @return string|null
*/
public function getMediaUri()
{
- $folder = $this->getMediaFolder();
+ $folder = $this->getMediaFolder();
+ if (!$folder) {
+ return null;
+ }
- if (strpos($folder, '://')) {
- return $folder;
- }
+ if (strpos($folder, '://')) {
+ return $folder;
+ }
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $user = $locator->findResource('user://');
- if (strpos($folder, $user) === 0) {
- return 'user://' . substr($folder, \strlen($user)+1);
- }
+ $locator = Grav::instance()['locator'];
+ $user = $locator->findResource('user://');
+ if (strpos($folder, $user) === 0) {
+ return 'user://' . substr($folder, strlen($user)+1);
+ }
- return null;
+ return null;
}
/**
* Gets the associated media collection.
*
- * @return MediaCollectionInterface Representation of associated media.
+ * @return MediaCollectionInterface|Media Representation of associated media.
*/
public function getMedia()
{
- if ($this->media === null) {
+ $media = $this->media;
+ if (null === $media) {
$cache = $this->getMediaCache();
+ $cacheKey = md5('media' . $this->getCacheKey());
// Use cached media if possible.
- $cacheKey = md5('media' . $this->getCacheKey());
- if (!$media = $cache->get($cacheKey)) {
- $media = new Media($this->getMediaFolder(), $this->getMediaOrder());
+ $media = $cache->get($cacheKey);
+ if (!$media instanceof MediaCollectionInterface) {
+ $media = new Media($this->getMediaFolder(), $this->getMediaOrder(), $this->_loadMedia);
$cache->set($cacheKey, $media);
}
+
$this->media = $media;
}
- return $this->media;
+ return $media;
}
/**
* Sets the associated media collection.
*
- * @param MediaCollectionInterface $media Representation of associated media.
+ * @param MediaCollectionInterface|Media $media Representation of associated media.
* @return $this
*/
protected function setMedia(MediaCollectionInterface $media)
@@ -99,8 +113,18 @@ trait MediaTrait
return $this;
}
+ /**
+ * @return void
+ */
+ protected function freeMedia()
+ {
+ $this->media = null;
+ }
+
/**
* Clear media cache.
+ *
+ * @return void
*/
protected function clearMediaCache()
{
@@ -108,7 +132,7 @@ trait MediaTrait
$cacheKey = md5('media' . $this->getCacheKey());
$cache->delete($cacheKey);
- $this->media = null;
+ $this->freeMedia();
}
/**
diff --git a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
new file mode 100644
index 00000000..7a1f55d6
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
@@ -0,0 +1,674 @@
+ true, // Whether path is in the media collection path itself.
+ 'avoid_overwriting' => false, // Do not override existing files (adds datetime postfix if conflict).
+ 'random_name' => false, // True if name needs to be randomized.
+ 'accept' => ['image/*'], // Accepted mime types or file extensions.
+ 'limit' => 10, // Maximum number of files.
+ 'filesize' => null, // Maximum filesize in MB.
+ 'destination' => null // Destination path, if empty, exception is thrown.
+ ];
+
+ /**
+ * Create Medium from an uploaded file.
+ *
+ * @param UploadedFileInterface $uploadedFile
+ * @param array $params
+ * @return Medium|null
+ */
+ public function createFromUploadedFile(UploadedFileInterface $uploadedFile, array $params = [])
+ {
+ return MediumFactory::fromUploadedFile($uploadedFile, $params);
+ }
+
+ /**
+ * Checks that uploaded file meets the requirements. Returns new filename.
+ *
+ * @example
+ * $filename = null; // Override filename if needed (ignored if randomizing filenames).
+ * $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
+ * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
+ * $media->copyUploadedFile($uploadedFile, $filename);
+ *
+ * @param UploadedFileInterface $uploadedFile
+ * @param string|null $filename
+ * @param array|null $settings
+ * @return string
+ * @throws RuntimeException
+ */
+ public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string
+ {
+ // Check if there is an upload error.
+ switch ($uploadedFile->getError()) {
+ case UPLOAD_ERR_OK:
+ break;
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
+ case UPLOAD_ERR_PARTIAL:
+ case UPLOAD_ERR_NO_FILE:
+ if (!$uploadedFile instanceof FormFlashFile) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400);
+ }
+ break;
+ case UPLOAD_ERR_NO_TMP_DIR:
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400);
+ case UPLOAD_ERR_CANT_WRITE:
+ case UPLOAD_ERR_EXTENSION:
+ default:
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
+ }
+
+ $metadata = [
+ 'filename' => $uploadedFile->getClientFilename(),
+ 'mime' => $uploadedFile->getClientMediaType(),
+ 'size' => $uploadedFile->getSize(),
+ ];
+
+ return $this->checkFileMetadata($metadata, $filename, $settings);
+ }
+
+ /**
+ * Checks that file metadata meets the requirements. Returns new filename.
+ *
+ * @param array $metadata
+ * @param array|null $settings
+ * @return string
+ * @throws RuntimeException
+ */
+ public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string
+ {
+ // Add the defaults to the settings.
+ $settings = $this->getUploadSettings($settings);
+
+ // Destination is always needed (but it can be set in defaults).
+ $self = $settings['self'] ?? false;
+ if (!isset($settings['destination']) && $self === false) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400);
+ }
+
+ if (null === $filename) {
+ // If no filename is given, use the filename from the uploaded file (path is not allowed).
+ $folder = '';
+ $filename = $metadata['filename'] ?? '';
+ } else {
+ // If caller sets the filename, we will accept any custom path.
+ $folder = dirname($filename);
+ if ($folder === '.') {
+ $folder = '';
+ }
+ $filename = basename($filename);
+ }
+ $extension = pathinfo($filename, PATHINFO_EXTENSION);
+
+ // Decide which filename to use.
+ if ($settings['random_name']) {
+ // Generate random filename if asked for.
+ $filename = mb_strtolower(Utils::generateRandomString(15) . '.' . $extension);
+ }
+
+ // Handle conflicting filename if needed.
+ if ($settings['avoid_overwriting']) {
+ $destination = $settings['destination'];
+ if ($destination && $this->fileExists($filename, $destination)) {
+ $filename = date('YmdHis') . '-' . $filename;
+ }
+ }
+ $filepath = $folder . $filename;
+
+ // Check if the filename is allowed.
+ if (!Utils::checkFilename($filename)) {
+ throw new RuntimeException(
+ sprintf($this->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filepath, $this->translate('PLUGIN_ADMIN.BAD_FILENAME'))
+ );
+ }
+
+ // Check if the file extension is allowed.
+ $extension = mb_strtolower($extension);
+ if (!$extension || !$this->getConfig()->get("media.types.{$extension}")) {
+ // Not a supported type.
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
+ }
+
+ // Calculate maximum file size (from MB).
+ $filesize = $settings['filesize'];
+ if ($filesize) {
+ $max_filesize = $filesize * 1048576;
+ if ($metadata['size'] > $max_filesize) {
+ // TODO: use own language string
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
+ }
+ } elseif (null === $filesize) {
+ // Check size against the Grav upload limit.
+ $grav_limit = Utils::getUploadLimit();
+ if ($grav_limit > 0 && $metadata['size'] > $grav_limit) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
+ }
+ }
+
+ $grav = Grav::instance();
+ /** @var MimeTypes $mimeChecker */
+ $mimeChecker = $grav['mime'];
+
+ // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
+ // Do not trust mime type sent by the browser.
+ $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
+ $validExtensions = $mimeChecker->getExtensions($mime);
+ if (!in_array($extension, $validExtensions, true)) {
+ throw new RuntimeException('The mime type does not match to file extension', 400);
+ }
+
+ $accepted = false;
+ $errors = [];
+ foreach ((array)$settings['accept'] as $type) {
+ // Force acceptance of any file when star notation
+ if ($type === '*') {
+ $accepted = true;
+ break;
+ }
+
+ $isMime = strstr($type, '/');
+ $find = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
+
+ if ($isMime) {
+ $match = preg_match('#' . $find . '$#', $mime);
+ if (!$match) {
+ // TODO: translate
+ $errors[] = 'The MIME type "' . $mime . '" for the file "' . $filepath . '" is not an accepted.';
+ } else {
+ $accepted = true;
+ break;
+ }
+ } else {
+ $match = preg_match('#' . $find . '$#', $filename);
+ if (!$match) {
+ // TODO: translate
+ $errors[] = 'The File Extension for the file "' . $filepath . '" is not an accepted.';
+ } else {
+ $accepted = true;
+ break;
+ }
+ }
+ }
+ if (!$accepted) {
+ throw new RuntimeException(implode('
', $errors), 400);
+ }
+
+ return $filepath;
+ }
+
+ /**
+ * Copy uploaded file to the media collection.
+ *
+ * WARNING: Always check uploaded file before copying it!
+ *
+ * @example
+ * $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
+ * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
+ * $media->copyUploadedFile($uploadedFile, $filename, $settings);
+ *
+ * @param UploadedFileInterface $uploadedFile
+ * @param string $filename
+ * @param array|null $settings
+ * @return void
+ * @throws RuntimeException
+ */
+ public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename, array $settings = null): void
+ {
+ // Add the defaults to the settings.
+ $settings = $this->getUploadSettings($settings);
+
+ $path = $settings['destination'] ?? $this->getPath();
+ if (!$path || !$filename) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
+ }
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ try {
+ // Clear locator cache to make sure we have up to date information from the filesystem.
+ $locator->clearCache();
+ $this->clearCache();
+
+ $filesystem = Filesystem::getInstance(false);
+
+ // Calculate path without the retina scaling factor.
+ $basename = $filesystem->basename($filename);
+ $pathname = $filesystem->pathname($filename);
+
+ // Get name for the uploaded file.
+ [$base, $ext,,] = $this->getFileParts($basename);
+ $name = "{$pathname}{$base}.{$ext}";
+
+ // Upload file.
+ if ($uploadedFile instanceof FormFlashFile) {
+ // FormFlashFile needs some additional logic.
+ if ($uploadedFile->getError() === \UPLOAD_ERR_OK) {
+ // Move uploaded file.
+ $this->doMoveUploadedFile($uploadedFile, $filename, $path);
+ } elseif (strpos($filename, 'original/') === 0 && !$this->fileExists($filename, $path) && $this->fileExists($basename, $path)) {
+ // Original image support: override original image if it's the same as the uploaded image.
+ $this->doCopy($basename, $filename, $path);
+ }
+
+ // FormFlashFile may also contain metadata.
+ $metadata = $uploadedFile->getMetaData();
+ if ($metadata) {
+ // TODO: This overrides metadata if used with multiple retina image sizes.
+ $this->doSaveMetadata(['upload' => $metadata], $name, $path);
+ }
+ } else {
+ // Not a FormFlashFile.
+ $this->doMoveUploadedFile($uploadedFile, $filename, $path);
+ }
+
+ // Post-processing: Special content sanitization for SVG.
+ $mime = Utils::getMimeByFilename($filename);
+ if (Utils::contains($mime, 'svg', false)) {
+ $this->doSanitizeSvg($filename, $path);
+ }
+
+ // Add the new file into the media.
+ // TODO: This overrides existing media sizes if used with multiple retina image sizes.
+ $this->doAddUploadedMedium($name, $filename, $path);
+
+ } catch (Exception $e) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE') . $e->getMessage(), 400);
+ } finally {
+ // Finally clear media cache.
+ $locator->clearCache();
+ $this->clearCache();
+ }
+ }
+
+ /**
+ * Delete real file from the media collection.
+ *
+ * @param string $filename
+ * @param array|null $settings
+ * @return void
+ * @throws RuntimeException
+ */
+ public function deleteFile(string $filename, array $settings = null): void
+ {
+ // Add the defaults to the settings.
+ $settings = $this->getUploadSettings($settings);
+ $filesystem = Filesystem::getInstance(false);
+
+ // First check for allowed filename.
+ $basename = $filesystem->basename($filename);
+ if (!Utils::checkFilename($basename)) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ": {$this->translate('PLUGIN_ADMIN.BAD_FILENAME')}: " . $filename, 400);
+ }
+
+ $path = $settings['destination'] ?? $this->getPath();
+ if (!$path) {
+ return;
+ }
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+ $locator->clearCache();
+
+ $pathname = $filesystem->pathname($filename);
+
+ // Get base name of the file.
+ [$base, $ext,,] = $this->getFileParts($basename);
+ $name = "{$pathname}{$base}.{$ext}";
+
+ // Remove file and all all the associated metadata.
+ $this->doRemove($name, $path);
+
+ // Finally clear media cache.
+ $locator->clearCache();
+ $this->clearCache();
+ }
+
+ /**
+ * Rename file inside the media collection.
+ *
+ * @param string $from
+ * @param string $to
+ * @param array|null $settings
+ */
+ public function renameFile(string $from, string $to, array $settings = null): void
+ {
+ // Add the defaults to the settings.
+ $settings = $this->getUploadSettings($settings);
+ $filesystem = Filesystem::getInstance(false);
+
+ $path = $settings['destination'] ?? $this->getPath();
+ if (!$path) {
+ // TODO: translate error message
+ throw new RuntimeException('Failed to rename file: Bad destination', 400);
+ }
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+ $locator->clearCache();
+
+ // Get base name of the file.
+ $pathname = $filesystem->pathname($from);
+
+ // Remove @2x, @3x and .meta.yaml
+ [$base, $ext,,] = $this->getFileParts($filesystem->basename($from));
+ $from = "{$pathname}{$base}.{$ext}";
+
+ [$base, $ext,,] = $this->getFileParts($filesystem->basename($to));
+ $to = "{$pathname}{$base}.{$ext}";
+
+ $this->doRename($from, $to, $path);
+
+ // Finally clear media cache.
+ $locator->clearCache();
+ $this->clearCache();
+ }
+
+ /**
+ * Internal logic to move uploaded file.
+ *
+ * @param UploadedFileInterface $uploadedFile
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doMoveUploadedFile(UploadedFileInterface $uploadedFile, string $filename, string $path): void
+ {
+ $filepath = sprintf('%s/%s', $path, $filename);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // Do not use streams internally.
+ if ($locator->isStream($filepath)) {
+ $filepath = (string)$locator->findResource($filepath, true, true);
+ }
+
+ Folder::create(dirname($filepath));
+
+ $uploadedFile->moveTo($filepath);
+ }
+
+ /**
+ * Get upload settings.
+ *
+ * @param array|null $settings Form field specific settings (override).
+ * @return array
+ */
+ public function getUploadSettings(?array $settings = null): array
+ {
+ return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
+ }
+
+ /**
+ * Internal logic to copy file.
+ *
+ * @param string $src
+ * @param string $dst
+ * @param string $path
+ */
+ protected function doCopy(string $src, string $dst, string $path): void
+ {
+ $src = sprintf('%s/%s', $path, $src);
+ $dst = sprintf('%s/%s', $path, $dst);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // Do not use streams internally.
+ if ($locator->isStream($dst)) {
+ $dst = (string)$locator->findResource($dst, true, true);
+ }
+
+ Folder::create(dirname($dst));
+
+ copy($src, $dst);
+ }
+
+ /**
+ * Internal logic to rename file.
+ *
+ * @param string $from
+ * @param string $to
+ * @param string $path
+ */
+ protected function doRename(string $from, string $to, string $path): void
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ $fromPath = $path . '/' . $from;
+ if ($locator->isStream($fromPath)) {
+ $fromPath = $locator->findResource($fromPath, true, true);
+ }
+
+ if (!is_file($fromPath)) {
+ return;
+ }
+
+ $mediaPath = dirname($fromPath);
+ $toPath = $mediaPath . '/' . $to;
+ if ($locator->isStream($toPath)) {
+ $toPath = $locator->findResource($toPath, true, true);
+ }
+
+ if (is_file($toPath)) {
+ // TODO: translate error message
+ throw new RuntimeException(sprintf('File could not be renamed: %s already exists (%s)', $to, $mediaPath), 500);
+ }
+
+ $result = rename($fromPath, $toPath);
+ if (!$result) {
+ // TODO: translate error message
+ throw new RuntimeException(sprintf('File could not be renamed: %s -> %s (%s)', $from, $to, $mediaPath), 500);
+ }
+
+ // TODO: Add missing logic to handle retina files.
+ if (is_file($fromPath . '.meta.yaml')) {
+ $result = rename($fromPath . '.meta.yaml', $toPath . '.meta.yaml');
+ if (!$result) {
+ // TODO: translate error message
+ throw new RuntimeException(sprintf('Meta could not be renamed: %s -> %s (%s)', $from, $to, $mediaPath), 500);
+ }
+ }
+ }
+
+ /**
+ * Internal logic to remove file.
+ *
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doRemove(string $filename, string $path): void
+ {
+ $filesystem = Filesystem::getInstance(false);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // If path doesn't exist, there's nothing to do.
+ $pathname = $filesystem->pathname($filename);
+ if (!$this->fileExists($pathname, $path)) {
+ return;
+ }
+
+ $folder = $locator->isStream($path) ? (string)$locator->findResource($path, true, true) : $path;
+
+ // Remove requested media file.
+ if ($this->fileExists($filename, $path)) {
+ $result = unlink("{$folder}/{$filename}");
+ if (!$result) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
+ }
+ }
+
+ // Remove associated metadata.
+ $this->doRemoveMetadata($filename, $path);
+
+ // Remove associated 2x, 3x and their .meta.yaml files.
+ $targetPath = rtrim(sprintf('%s/%s', $folder, $pathname), '/');
+ $dir = scandir($targetPath, SCANDIR_SORT_NONE);
+ if (false === $dir) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
+ }
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ $basename = $filesystem->basename($filename);
+ $fileParts = (array)$filesystem->pathinfo($filename);
+
+ foreach ($dir as $file) {
+ $preg_name = preg_quote($fileParts['filename'], '`');
+ $preg_ext = preg_quote($fileParts['extension'] ?? '.', '`');
+ $preg_filename = preg_quote($basename, '`');
+
+ if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) {
+ $testPath = $targetPath . '/' . $file;
+ if ($locator->isStream($testPath)) {
+ $testPath = (string)$locator->findResource($testPath, true, true);
+ $locator->clearCache($testPath);
+ }
+
+ if (is_file($testPath)) {
+ $result = unlink($testPath);
+ if (!$result) {
+ throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param array $metadata
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doSaveMetadata(array $metadata, string $filename, string $path): void
+ {
+ $filepath = sprintf('%s/%s', $path, $filename);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // Do not use streams internally.
+ if ($locator->isStream($filepath)) {
+ $filepath = (string)$locator->findResource($filepath, true, true);
+ }
+
+ $file = YamlFile::instance($filepath . '.meta.yaml');
+ $file->save($metadata);
+ }
+
+ /**
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doRemoveMetadata(string $filename, string $path): void
+ {
+ $filepath = sprintf('%s/%s', $path, $filename);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // Do not use streams internally.
+ if ($locator->isStream($filepath)) {
+ $filepath = (string)$locator->findResource($filepath, true);
+ if (!$filepath) {
+ return;
+ }
+ }
+
+ $file = YamlFile::instance($filepath . '.meta.yaml');
+ if ($file->exists()) {
+ $file->delete();
+ }
+ }
+
+ /**
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doSanitizeSvg(string $filename, string $path): void
+ {
+ $filepath = sprintf('%s/%s', $path, $filename);
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+
+ // Do not use streams internally.
+ if ($locator->isStream($filepath)) {
+ $filepath = (string)$locator->findResource($filepath, true, true);
+ }
+
+ Security::sanitizeSVG($filepath);
+ }
+
+ /**
+ * @param string $name
+ * @param string $filename
+ * @param string $path
+ */
+ protected function doAddUploadedMedium(string $name, string $filename, string $path): void
+ {
+ $filepath = sprintf('%s/%s', $path, $filename);
+ $medium = $this->createFromFile($filepath);
+ $realpath = $path . '/' . $name;
+ $this->add($realpath, $medium);
+ }
+
+ /**
+ * @param string $string
+ * @return string
+ */
+ protected function translate(string $string): string
+ {
+ return $this->getLanguage()->translate($string);
+ }
+
+ abstract protected function getPath(): ?string;
+
+ abstract protected function getGrav(): Grav;
+
+ abstract protected function getConfig(): Config;
+
+ abstract protected function getLanguage(): Language;
+
+ abstract protected function clearCache(): void;
+}
diff --git a/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php
new file mode 100644
index 00000000..49d75be6
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php
@@ -0,0 +1,40 @@
+styleAttributes['width'] = $width . 'px';
+ } else {
+ unset($this->styleAttributes['width']);
+ }
+ if ($height) {
+ $this->styleAttributes['height'] = $height . 'px';
+ } else {
+ unset($this->styleAttributes['height']);
+ }
+
+ return $this;
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php
new file mode 100644
index 00000000..eab73200
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php
@@ -0,0 +1,149 @@
+bubble('parsedownElement', [$title, $alt, $class, $id, $reset]);
+ }
+
+ /**
+ * Return HTML markup from the medium.
+ *
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
+ * @param bool $reset
+ * @return string
+ */
+ public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
+ {
+ return $this->bubble('html', [$title, $alt, $class, $id, $reset]);
+ }
+
+ /**
+ * Switch display mode.
+ *
+ * @param string $mode
+ *
+ * @return MediaLinkInterface|MediaObjectInterface|null
+ */
+ public function display($mode = 'source')
+ {
+ return $this->bubble('display', [$mode], false);
+ }
+
+ /**
+ * Switch thumbnail.
+ *
+ * @param string $type
+ *
+ * @return MediaLinkInterface|MediaObjectInterface
+ */
+ public function thumbnail($type = 'auto')
+ {
+ $this->bubble('thumbnail', [$type], false);
+
+ return $this->bubble('getThumbnail', [], false);
+ }
+
+ /**
+ * Turn the current Medium into a Link
+ *
+ * @param bool $reset
+ * @param array $attributes
+ * @return MediaLinkInterface
+ */
+ public function link($reset = true, array $attributes = [])
+ {
+ return $this->bubble('link', [$reset, $attributes], false);
+ }
+
+ /**
+ * Turn the current Medium into a Link with lightbox enabled
+ *
+ * @param int|null $width
+ * @param int|null $height
+ * @param bool $reset
+ * @return MediaLinkInterface
+ */
+ public function lightbox($width = null, $height = null, $reset = true)
+ {
+ return $this->bubble('lightbox', [$width, $height, $reset], false);
+ }
+
+ /**
+ * Bubble a function call up to either the superclass function or the parent Medium instance
+ *
+ * @param string $method
+ * @param array $arguments
+ * @param bool $testLinked
+ * @return mixed
+ */
+ protected function bubble($method, array $arguments = [], $testLinked = true)
+ {
+ if (!$testLinked || $this->linked) {
+ $parent = $this->parent;
+ if (null === $parent) {
+ return $this;
+ }
+
+ $closure = [$parent, $method];
+
+ if (!is_callable($closure)) {
+ throw new BadMethodCallException(get_class($parent) . '::' . $method . '() not found.');
+ }
+
+ return $closure(...$arguments);
+ }
+
+ return parent::{$method}(...$arguments);
+ }
+}
diff --git a/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php
new file mode 100644
index 00000000..e03fbbd8
--- /dev/null
+++ b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php
@@ -0,0 +1,68 @@
+attributes['poster'] = $urlImage;
+
+ return $this;
+ }
+
+ /**
+ * Allows to set the playsinline attribute
+ *
+ * @param bool $status
+ * @return $this
+ */
+ public function playsinline($status = false)
+ {
+ if ($status) {
+ $this->attributes['playsinline'] = true;
+ } else {
+ unset($this->attributes['playsinline']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Parsedown element for source display mode
+ *
+ * @param array $attributes
+ * @param bool $reset
+ * @return array
+ */
+ protected function sourceParsedownElement(array $attributes, $reset = true)
+ {
+ $location = $this->url($reset);
+
+ return [
+ 'name' => 'video',
+ 'rawHtml' => 'Your browser does not support the video tag.',
+ 'attributes' => $attributes
+ ];
+ }
+}
diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php
index 51de16b0..32b917cd 100644
--- a/system/src/Grav/Common/Page/Collection.php
+++ b/system/src/Grav/Common/Page/Collection.php
@@ -3,27 +3,36 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use Exception;
use Grav\Common\Grav;
use Grav\Common\Iterator;
+use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
+use InvalidArgumentException;
+use function array_key_exists;
+use function array_keys;
+use function array_search;
+use function count;
+use function in_array;
+use function is_array;
+use function is_string;
-class Collection extends Iterator
+/**
+ * Class Collection
+ * @package Grav\Common\Page
+ */
+class Collection extends Iterator implements PageCollectionInterface
{
- /**
- * @var Pages
- */
+ /** @var Pages */
protected $pages;
-
- /**
- * @var array
- */
+ /** @var array */
protected $params;
/**
@@ -38,7 +47,7 @@ class Collection extends Iterator
parent::__construct($items);
$this->params = $params;
- $this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages');
+ $this->pages = $pages ?: Grav::instance()->offsetGet('pages');
}
/**
@@ -51,11 +60,23 @@ class Collection extends Iterator
return $this->params;
}
+ /**
+ * Set parameters to the Collection
+ *
+ * @param array $params
+ * @return $this
+ */
+ public function setParams(array $params)
+ {
+ $this->params = array_merge($this->params, $params);
+
+ return $this;
+ }
+
/**
* Add a single page to a collection
*
* @param PageInterface $page
- *
* @return $this
*/
public function addPage(PageInterface $page)
@@ -94,12 +115,12 @@ class Collection extends Iterator
*
* Merge another collection with the current collection
*
- * @param Collection $collection
+ * @param PageCollectionInterface $collection
* @return $this
*/
- public function merge(Collection $collection)
+ public function merge(PageCollectionInterface $collection)
{
- foreach($collection as $page) {
+ foreach ($collection as $page) {
$this->addPage($page);
}
@@ -109,15 +130,15 @@ class Collection extends Iterator
/**
* Intersect another collection with the current collection
*
- * @param Collection $collection
+ * @param PageCollectionInterface $collection
* @return $this
*/
- public function intersect(Collection $collection)
+ public function intersect(PageCollectionInterface $collection)
{
$array1 = $this->items;
$array2 = $collection->toArray();
- $this->items = array_uintersect($array1, $array2, function($val1, $val2) {
+ $this->items = array_uintersect($array1, $array2, function ($val1, $val2) {
return strcmp($val1['slug'], $val2['slug']);
});
@@ -125,17 +146,15 @@ class Collection extends Iterator
}
/**
- * Set parameters to the Collection
- *
- * @param array $params
- *
- * @return $this
+ * Set current page.
*/
- public function setParams(array $params)
+ public function setCurrent(string $path): void
{
- $this->params = array_merge($this->params, $params);
+ reset($this->items);
- return $this;
+ while (($key = key($this->items)) !== null && $key !== $path) {
+ next($this->items);
+ }
}
/**
@@ -165,9 +184,8 @@ class Collection extends Iterator
/**
* Returns the value at specified offset.
*
- * @param mixed $offset The offset to retrieve.
- *
- * @return mixed Can return all value types.
+ * @param string $offset
+ * @return PageInterface|null
*/
public function offsetGet($offset)
{
@@ -178,7 +196,7 @@ class Collection extends Iterator
* Split collection into array of smaller collections.
*
* @param int $size
- * @return array|Collection[]
+ * @return Collection[]
*/
public function batch($size)
{
@@ -196,9 +214,8 @@ class Collection extends Iterator
* Remove item from the list.
*
* @param PageInterface|string|null $key
- *
* @return $this
- * @throws \InvalidArgumentException
+ * @throws InvalidArgumentException
*/
public function remove($key = null)
{
@@ -207,8 +224,8 @@ class Collection extends Iterator
} elseif (null === $key) {
$key = (string)key($this->items);
}
- if (!\is_string($key)) {
- throw new \InvalidArgumentException('Invalid argument $key.');
+ if (!is_string($key)) {
+ throw new InvalidArgumentException('Invalid argument $key.');
}
parent::remove($key);
@@ -221,9 +238,8 @@ class Collection extends Iterator
*
* @param string $by
* @param string $dir
- * @param array $manual
- * @param string $sort_flags
- *
+ * @param array|null $manual
+ * @param string|null $sort_flags
* @return $this
*/
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
@@ -237,10 +253,9 @@ class Collection extends Iterator
* Check to see if this item is the first in the collection.
*
* @param string $path
- *
* @return bool True if item is first.
*/
- public function isFirst($path)
+ public function isFirst($path): bool
{
return $this->items && $path === array_keys($this->items)[0];
}
@@ -249,12 +264,11 @@ class Collection extends Iterator
* Check to see if this item is the last in the collection.
*
* @param string $path
- *
* @return bool True if item is last.
*/
- public function isLast($path)
+ public function isLast($path): bool
{
- return $this->items && $path === array_keys($this->items)[\count($this->items) - 1];
+ return $this->items && $path === array_keys($this->items)[count($this->items) - 1];
}
/**
@@ -286,7 +300,6 @@ class Collection extends Iterator
*
* @param string $path
* @param int $direction either -1 or +1
- *
* @return PageInterface|Collection The sibling item.
*/
public function adjacentSibling($path, $direction = 1)
@@ -301,48 +314,49 @@ class Collection extends Iterator
}
return $this;
-
}
/**
* Returns the item in the current position.
*
* @param string $path the path the item
- *
- * @return int the index of the current page.
+ * @return int|null The index of the current page, null if not found.
*/
- public function currentPosition($path)
+ public function currentPosition($path): ?int
{
- return \array_search($path, \array_keys($this->items), true);
+ $pos = array_search($path, array_keys($this->items), true);
+
+ return $pos !== false ? $pos : null;
}
/**
* Returns the items between a set of date ranges of either the page date field (default) or
- * an arbitrary datetime page field where end date is optional
- * Dates can be passed in as text that strtotime() can process
+ * an arbitrary datetime page field where start date and end date are optional
+ * Dates must be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
- * @param string $startDate
- * @param bool $endDate
+ * @param string|null $startDate
+ * @param string|null $endDate
* @param string|null $field
- *
* @return $this
- * @throws \Exception
+ * @throws Exception
*/
- public function dateRange($startDate, $endDate = false, $field = null)
+ public function dateRange($startDate = null, $endDate = null, $field = null)
{
- $start = Utils::date2timestamp($startDate);
- $end = $endDate ? Utils::date2timestamp($endDate) : false;
+ $start = $startDate ? Utils::date2timestamp($startDate) : null;
+ $end = $endDate ? Utils::date2timestamp($endDate) : null;
$date_range = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
- if ($page !== null) {
- $date = $field ? strtotime($page->value($field)) : $page->date();
+ if (!$page) {
+ continue;
+ }
- if ($date >= $start && (!$end || $date <= $end)) {
- $date_range[$path] = $slug;
- }
+ $date = $field ? strtotime($page->value($field)) : $page->date();
+
+ if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
+ $date_range[$path] = $slug;
}
}
@@ -392,17 +406,17 @@ class Collection extends Iterator
}
/**
- * Creates new collection with only modular pages
+ * Creates new collection with only pages
*
- * @return Collection The collection with only modular pages
+ * @return Collection The collection with only pages
*/
- public function modular()
+ public function pages()
{
$modular = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
- if ($page !== null && $page->modular()) {
+ if ($page !== null && !$page->isModule()) {
$modular[$path] = $slug;
}
}
@@ -412,17 +426,17 @@ class Collection extends Iterator
}
/**
- * Creates new collection with only non-modular pages
+ * Creates new collection with only modules
*
- * @return Collection The collection with only non-modular pages
+ * @return Collection The collection with only modules
*/
- public function nonModular()
+ public function modules()
{
$modular = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
- if ($page !== null && !$page->modular()) {
+ if ($page !== null && $page->isModule()) {
$modular[$path] = $slug;
}
}
@@ -431,6 +445,72 @@ class Collection extends Iterator
return $this;
}
+ /**
+ * Alias of pages()
+ *
+ * @return Collection The collection with only non-module pages
+ */
+ public function nonModular()
+ {
+ $this->pages();
+
+ return $this;
+ }
+
+ /**
+ * Alias of modules()
+ *
+ * @return Collection The collection with only modules
+ */
+ public function modular()
+ {
+ $this->modules();
+
+ return $this;
+ }
+
+ /**
+ * Creates new collection with only translated pages
+ *
+ * @return Collection The collection with only published pages
+ * @internal
+ */
+ public function translated()
+ {
+ $published = [];
+
+ foreach ($this->items as $path => $slug) {
+ $page = $this->pages->get($path);
+ if ($page !== null && $page->translated()) {
+ $published[$path] = $slug;
+ }
+ }
+ $this->items = $published;
+
+ return $this;
+ }
+
+ /**
+ * Creates new collection with only untranslated pages
+ *
+ * @return Collection The collection with only non-published pages
+ * @internal
+ */
+ public function nonTranslated()
+ {
+ $published = [];
+
+ foreach ($this->items as $path => $slug) {
+ $page = $this->pages->get($path);
+ if ($page !== null && !$page->translated()) {
+ $published[$path] = $slug;
+ }
+ }
+ $this->items = $published;
+
+ return $this;
+ }
+
/**
* Creates new collection with only published pages
*
@@ -517,7 +597,6 @@ class Collection extends Iterator
* Creates new collection with only pages of the specified type
*
* @param string $type
- *
* @return Collection The collection
*/
public function ofType($type)
@@ -540,7 +619,6 @@ class Collection extends Iterator
* Creates new collection with only pages of one of the specified types
*
* @param string[] $types
- *
* @return Collection The collection
*/
public function ofOneOfTheseTypes($types)
@@ -549,7 +627,7 @@ class Collection extends Iterator
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
- if ($page !== null && \in_array($page->template(), $types, true)) {
+ if ($page !== null && in_array($page->template(), $types, true)) {
$items[$path] = $slug;
}
}
@@ -563,7 +641,6 @@ class Collection extends Iterator
* Creates new collection with only pages of one of the specified access levels
*
* @param array $accessLevels
- *
* @return Collection The collection
*/
public function ofOneOfTheseAccessLevels($accessLevels)
@@ -574,19 +651,19 @@ class Collection extends Iterator
$page = $this->pages->get($path);
if ($page !== null && isset($page->header()->access)) {
- if (\is_array($page->header()->access)) {
+ if (is_array($page->header()->access)) {
//Multiple values for access
$valid = false;
foreach ($page->header()->access as $index => $accessLevel) {
- if (\is_array($accessLevel)) {
+ if (is_array($accessLevel)) {
foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
- if (\in_array($innerAccessLevel, $accessLevels)) {
+ if (in_array($innerAccessLevel, $accessLevels, false)) {
$valid = true;
}
}
} else {
- if (\in_array($index, $accessLevels)) {
+ if (in_array($index, $accessLevels, false)) {
$valid = true;
}
}
@@ -596,11 +673,10 @@ class Collection extends Iterator
}
} else {
//Single value for access
- if (\in_array($page->header()->access, $accessLevels)) {
+ if (in_array($page->header()->access, $accessLevels, false)) {
$items[$path] = $slug;
}
}
-
}
}
@@ -613,7 +689,7 @@ class Collection extends Iterator
* Get the extended version of this Collection with each page keyed by route
*
* @return array
- * @throws \Exception
+ * @throws Exception
*/
public function toExtendedArray()
{
diff --git a/system/src/Grav/Common/Page/Header.php b/system/src/Grav/Common/Page/Header.php
index e3854aec..71c7b16c 100644
--- a/system/src/Grav/Common/Page/Header.php
+++ b/system/src/Grav/Common/Page/Header.php
@@ -3,16 +3,35 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use ArrayAccess;
+use JsonSerializable;
use RocketTheme\Toolbox\ArrayTraits\Constructor;
-use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccess;
+use RocketTheme\Toolbox\ArrayTraits\Export;
+use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
+use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
-class Header implements \ArrayAccess
+/**
+ * Class Header
+ * @package Grav\Common\Page
+ */
+class Header implements ArrayAccess, ExportInterface, JsonSerializable
{
- use NestedArrayAccess, Constructor;
+ use NestedArrayAccessWithGetters, Constructor, Export;
+
+ /** @var array */
+ protected $items;
+
+ /**
+ * @return array
+ */
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
}
diff --git a/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php
new file mode 100644
index 00000000..50029118
--- /dev/null
+++ b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php
@@ -0,0 +1,283 @@
+modules() instead
+ */
+ public function modular();
+
+ /**
+ * Creates new collection with only non-module pages
+ *
+ * @return PageCollectionInterface The collection with only non-module pages
+ * @deprecated 1.7 Use $this->pages() instead
+ */
+ public function nonModular();
+
+ /**
+ * Creates new collection with only published pages
+ *
+ * @return PageCollectionInterface The collection with only published pages
+ */
+ public function published();
+
+ /**
+ * Creates new collection with only non-published pages
+ *
+ * @return PageCollectionInterface The collection with only non-published pages
+ */
+ public function nonPublished();
+
+ /**
+ * Creates new collection with only routable pages
+ *
+ * @return PageCollectionInterface The collection with only routable pages
+ */
+ public function routable();
+
+ /**
+ * Creates new collection with only non-routable pages
+ *
+ * @return PageCollectionInterface The collection with only non-routable pages
+ */
+ public function nonRoutable();
+
+ /**
+ * Creates new collection with only pages of the specified type
+ *
+ * @param string $type
+ * @return PageCollectionInterface The collection
+ */
+ public function ofType($type);
+
+ /**
+ * Creates new collection with only pages of one of the specified types
+ *
+ * @param string[] $types
+ * @return PageCollectionInterface The collection
+ */
+ public function ofOneOfTheseTypes($types);
+
+ /**
+ * Creates new collection with only pages of one of the specified access levels
+ *
+ * @param array $accessLevels
+ * @return PageCollectionInterface The collection
+ */
+ public function ofOneOfTheseAccessLevels($accessLevels);
+
+ /**
+ * Converts collection into an array.
+ *
+ * @return array
+ */
+ public function toArray();
+
+ /**
+ * Get the extended version of this Collection with each page keyed by route
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function toExtendedArray();
+}
diff --git a/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php b/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php
index 8a3ee83b..5156ede2 100644
--- a/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php
+++ b/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php
@@ -3,13 +3,13 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Interfaces;
-use Grav\Common\Page\Media;
+use Grav\Common\Media\Interfaces\MediaCollectionInterface;
/**
* Methods currently implemented in Flex Page emulation layer.
@@ -19,8 +19,7 @@ interface PageContentInterface
/**
* Gets and Sets the header based on the YAML configuration at the top of the .md file
*
- * @param object|array $var a YAML object representing the configuration for the file
- *
+ * @param object|array|null $var a YAML object representing the configuration for the file
* @return object the current YAML configuration
*/
public function header($var = null);
@@ -28,19 +27,23 @@ interface PageContentInterface
/**
* Get the summary.
*
- * @param int $size Max summary size.
- *
+ * @param int|null $size Max summary size.
* @param bool $textOnly Only count text size.
- *
* @return string
*/
public function summary($size = null, $textOnly = false);
/**
- * Gets and Sets the content based on content portion of the .md file
+ * Sets the summary of the page
*
- * @param string $var Content
+ * @param string $summary Summary
+ */
+ public function setSummary($summary);
+
+ /**
+ * Gets and Sets the content based on content portion of the .md file
*
+ * @param string|null $var Content
* @return string Content
*/
public function content($var = null);
@@ -63,8 +66,7 @@ interface PageContentInterface
* Gets and Sets the Page raw content
*
* @param string|null $var
- *
- * @return null
+ * @return string
*/
public function rawMarkdown($var = null);
@@ -72,8 +74,7 @@ interface PageContentInterface
* Get value from a page variable (used mostly for creating edit forms).
*
* @param string $name Variable name.
- * @param mixed $default
- *
+ * @param mixed|null $default
* @return mixed
*/
public function value($name, $default = null);
@@ -81,18 +82,16 @@ interface PageContentInterface
/**
* Gets and sets the associated media as found in the page folder.
*
- * @param Media $var Representation of associated media.
- *
- * @return Media Representation of associated media.
+ * @param MediaCollectionInterface|null $var New media object.
+ * @return MediaCollectionInterface Representation of associated media.
*/
public function media($var = null);
/**
* Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name
*
- * @param string $var the title of the Page
- *
- * @return string the title of the Page
+ * @param string|null $var New title of the Page
+ * @return string The title of the Page
*/
public function title($var = null);
@@ -100,45 +99,40 @@ interface PageContentInterface
* Gets and sets the menu name for this Page. This is the text that can be used specifically for navigation.
* If no menu field is set, it will use the title()
*
- * @param string $var the menu field for the page
- *
- * @return string the menu field for the page
+ * @param string|null $var New menu field for the page
+ * @return string The menu field for the page
*/
public function menu($var = null);
/**
* Gets and Sets whether or not this Page is visible for navigation
*
- * @param bool $var true if the page is visible
- *
- * @return bool true if the page is visible
+ * @param bool|null $var New value
+ * @return bool True if the page is visible
*/
public function visible($var = null);
/**
* Gets and Sets whether or not this Page is considered published
*
- * @param bool $var true if the page is published
- *
- * @return bool true if the page is published
+ * @param bool|null $var New value
+ * @return bool True if the page is published
*/
public function published($var = null);
/**
* Gets and Sets the Page publish date
*
- * @param string $var string representation of a date
- *
- * @return int unix timestamp representation of the date
+ * @param string|null $var String representation of the new date
+ * @return int Unix timestamp representation of the date
*/
public function publishDate($var = null);
/**
* Gets and Sets the Page unpublish date
*
- * @param string $var string representation of a date
- *
- * @return int|null unix timestamp representation of the date
+ * @param string|null $var String representation of the new date
+ * @return int|null Unix timestamp representation of the date
*/
public function unpublishDate($var = null);
@@ -146,9 +140,8 @@ interface PageContentInterface
* Gets and Sets the process setup for this Page. This is multi-dimensional array that consists of
* a simple array of arrays with the form array("markdown"=>true) for example
*
- * @param array $var an Array of name value pairs where the name is the process and value is true or false
- *
- * @return array an Array of name value pairs where the name is the process and value is true or false
+ * @param array|null $var New array of name value pairs where the name is the process and value is true or false
+ * @return array Array of name value pairs where the name is the process and value is true or false
*/
public function process($var = null);
@@ -156,63 +149,56 @@ interface PageContentInterface
* Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses
* the parent folder from the path
*
- * @param string $var the slug, e.g. 'my-blog'
- *
- * @return string the slug
+ * @param string|null $var New slug, e.g. 'my-blog'
+ * @return string The slug
*/
public function slug($var = null);
/**
* Get/set order number of this page.
*
- * @param int $var
- *
- * @return int|bool
+ * @param int|null $var New order as a number
+ * @return string|bool Order in a form of '02.' or false if not set
*/
public function order($var = null);
/**
* Gets and sets the identifier for this Page object.
*
- * @param string $var the identifier
- *
- * @return string the identifier
+ * @param string|null $var New identifier
+ * @return string The identifier
*/
public function id($var = null);
/**
* Gets and sets the modified timestamp.
*
- * @param int $var modified unix timestamp
- *
- * @return int modified unix timestamp
+ * @param int|null $var New modified unix timestamp
+ * @return int Modified unix timestamp
*/
public function modified($var = null);
/**
* Gets and sets the option to show the last_modified header for the page.
*
- * @param boolean $var show last_modified header
- *
- * @return boolean show last_modified header
+ * @param bool|null $var New last_modified header value
+ * @return bool Show last_modified header
*/
public function lastModified($var = null);
/**
* Get/set the folder.
*
- * @param string $var Optional path
- *
- * @return string|null
+ * @param string|null $var New folder
+ * @return string|null The folder
*/
public function folder($var = null);
/**
* Gets and sets the date for this Page object. This is typically passed in via the page headers
*
- * @param string $var string representation of a date
- *
- * @return int unix timestamp representation of the date
+ * @param string|null $var New string representation of a date
+ * @return int Unix timestamp representation of the date
*/
public function date($var = null);
@@ -220,30 +206,34 @@ interface PageContentInterface
* Gets and sets the date format for this Page object. This is typically passed in via the page headers
* using typical PHP date string structure - http://php.net/manual/en/function.date.php
*
- * @param string $var string representation of a date format
- *
- * @return string string representation of a date format
+ * @param string|null $var New string representation of a date format
+ * @return string String representation of a date format
*/
public function dateformat($var = null);
/**
* Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with.
*
- * @param array $var an array of taxonomies
- *
- * @return array an array of taxonomies
+ * @param array|null $var New array of taxonomies
+ * @return array An array of taxonomies
*/
public function taxonomy($var = null);
/**
* Gets the configured state of the processing method.
*
- * @param string $process the process, eg "twig" or "markdown"
- *
- * @return bool whether or not the processing method is enabled for this Page
+ * @param string $process The process name, eg "twig" or "markdown"
+ * @return bool Whether or not the processing method is enabled for this Page
*/
public function shouldProcess($process);
+ /**
+ * Returns true if page is a module.
+ *
+ * @return bool
+ */
+ public function isModule(): bool;
+
/**
* Returns whether or not this Page object has a .md file associated with it or if its just a directory.
*
diff --git a/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php b/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php
new file mode 100644
index 00000000..3c88ebf6
--- /dev/null
+++ b/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php
@@ -0,0 +1,33 @@
+ blueprint, ...], where blueprint follows the regular form blueprint format.
+ *
+ * @return array
+ */
+ //public function getForms(): array;
+
+ /**
+ * Add forms to this page.
+ *
+ * @param array $new
+ * @return $this
+ */
+ public function addForms(array $new/*, $override = true*/);
+
+ /**
+ * Alias of $this->getForms();
+ *
+ * @return array
+ */
+ public function forms();//: array;
+}
diff --git a/system/src/Grav/Common/Page/Interfaces/PageInterface.php b/system/src/Grav/Common/Page/Interfaces/PageInterface.php
index 24bf50ce..e5ea20c4 100644
--- a/system/src/Grav/Common/Page/Interfaces/PageInterface.php
+++ b/system/src/Grav/Common/Page/Interfaces/PageInterface.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,6 +14,12 @@ use Grav\Common\Media\Interfaces\MediaInterface;
/**
* Class implements page interface.
*/
-interface PageInterface extends PageContentInterface, PageRoutableInterface, PageTranslateInterface, MediaInterface, PageLegacyInterface
+interface PageInterface extends
+ PageContentInterface,
+ PageFormInterface,
+ PageRoutableInterface,
+ PageTranslateInterface,
+ MediaInterface,
+ PageLegacyInterface
{
}
diff --git a/system/src/Grav/Common/Page/Interfaces/PageLegacyInterface.php b/system/src/Grav/Common/Page/Interfaces/PageLegacyInterface.php
index 470d80d7..0d38c7d3 100644
--- a/system/src/Grav/Common/Page/Interfaces/PageLegacyInterface.php
+++ b/system/src/Grav/Common/Page/Interfaces/PageLegacyInterface.php
@@ -4,25 +4,29 @@ namespace Grav\Common\Page\Interfaces;
use Exception;
use Grav\Common\Data\Blueprint;
use Grav\Common\Page\Collection;
+use InvalidArgumentException;
use RocketTheme\Toolbox\File\MarkdownFile;
+use SplFileInfo;
+/**
+ * Interface PageLegacyInterface
+ * @package Grav\Common\Page\Interfaces
+ */
interface PageLegacyInterface
{
/**
* Initializes the page instance variables based on a file
*
- * @param \SplFileInfo $file The file information for the .md file that the page represents
- * @param string $extension
- *
+ * @param SplFileInfo $file The file information for the .md file that the page represents
+ * @param string|null $extension
* @return $this
*/
- public function init(\SplFileInfo $file, $extension = null);
+ public function init(SplFileInfo $file, $extension = null);
/**
* Gets and Sets the raw data
*
- * @param string $var Raw content string
- *
+ * @param string|null $var Raw content string
* @return string Raw content string
*/
public function raw($var = null);
@@ -31,7 +35,6 @@ interface PageLegacyInterface
* Gets and Sets the page frontmatter
*
* @param string|null $var
- *
* @return string
*/
public function frontmatter($var = null);
@@ -49,14 +52,10 @@ interface PageLegacyInterface
*/
public function httpResponseCode();
- public function httpHeaders();
-
/**
- * Sets the summary of the page
- *
- * @param string $summary Summary
+ * @return array
*/
- public function setSummary($summary);
+ public function httpHeaders();
/**
* Get the contentMeta array and initialize content first if it's not already
@@ -69,7 +68,7 @@ interface PageLegacyInterface
* Add an entry to the page's contentMeta array
*
* @param string $name
- * @param string $value
+ * @param mixed $value
*/
public function addContentMeta($name, $value);
@@ -77,7 +76,6 @@ interface PageLegacyInterface
* Return the whole contentMeta array as it currently stands
*
* @param string|null $name
- *
* @return mixed
*/
public function getContentMeta($name = null);
@@ -86,7 +84,6 @@ interface PageLegacyInterface
* Sets the whole content meta array in one shot
*
* @param array $content_meta
- *
* @return array
*/
public function setContentMeta($content_meta);
@@ -116,7 +113,6 @@ interface PageLegacyInterface
* You need to call $this->save() in order to perform the move.
*
* @param PageInterface $parent New parent page.
- *
* @return $this
*/
public function move(PageInterface $parent);
@@ -128,7 +124,6 @@ interface PageLegacyInterface
* You need to call $this->save() in order to perform the move.
*
* @param PageInterface $parent New parent page.
- *
* @return $this
*/
public function copy(PageInterface $parent);
@@ -202,8 +197,7 @@ interface PageLegacyInterface
/**
* Gets and sets the name field. If no name field is set, it will return 'default.md'.
*
- * @param string $var The name of this page.
- *
+ * @param string|null $var The name of this page.
* @return string The name of this page.
*/
public function name($var = null);
@@ -219,8 +213,7 @@ interface PageLegacyInterface
* Gets and sets the template field. This is used to find the correct Twig template file to render.
* If no field is set, it will return the name without the .md extension
*
- * @param string $var the template name
- *
+ * @param string|null $var the template name
* @return string the template name
*/
public function template($var = null);
@@ -229,26 +222,23 @@ interface PageLegacyInterface
* Allows a page to override the output render format, usually the extension provided
* in the URL. (e.g. `html`, `json`, `xml`, etc).
*
- * @param null $var
- *
- * @return null
+ * @param string|null $var
+ * @return string
*/
public function templateFormat($var = null);
/**
* Gets and sets the extension field.
*
- * @param null $var
- *
- * @return null|string
+ * @param string|null $var
+ * @return string|null
*/
public function extension($var = null);
/**
* Gets and sets the expires field. If not set will return the default
*
- * @param int $var The new expires value.
- *
+ * @param int|null $var The new expires value.
* @return int The expires value
*/
public function expires($var = null);
@@ -257,17 +247,21 @@ interface PageLegacyInterface
* Gets and sets the cache-control property. If not set it will return the default value (null)
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options
*
- * @param null $var
- * @return null
+ * @param string|null $var
+ * @return string|null
*/
public function cacheControl($var = null);
+ /**
+ * @param bool|null $var
+ * @return bool
+ */
public function ssl($var = null);
/**
* Returns the state of the debugger override etting for this page
*
- * @return mixed
+ * @return bool
*/
public function debugger();
@@ -275,8 +269,7 @@ interface PageLegacyInterface
* Function to merge page metadata tags and build an array of Metadata objects
* that can then be rendered in the page.
*
- * @param array $var an Array of metadata values to set
- *
+ * @param array|null $var an Array of metadata values to set
* @return array an Array of metadata values for the page
*/
public function metadata($var = null);
@@ -284,17 +277,15 @@ interface PageLegacyInterface
/**
* Gets and sets the option to show the etag header for the page.
*
- * @param bool $var show etag header
- *
+ * @param bool|null $var show etag header
* @return bool show etag header
*/
- public function eTag($var = null);
+ public function eTag($var = null): bool;
/**
* Gets and sets the path to the .md file for this Page object.
*
- * @param string $var the file path
- *
+ * @param string|null $var the file path
* @return string|null the file path
*/
public function filePath($var = null);
@@ -309,8 +300,7 @@ interface PageLegacyInterface
/**
* Gets and sets the order by which any sub-pages should be sorted.
*
- * @param string $var the order, either "asc" or "desc"
- *
+ * @param string|null $var the order, either "asc" or "desc"
* @return string the order, either "asc" or "desc"
* @deprecated 1.6
*/
@@ -324,8 +314,7 @@ interface PageLegacyInterface
* date - is the order based on the date set in the pages
* folder - is the order based on the name of the folder with any numerics omitted
*
- * @param string $var supported options include "default", "title", "date", and "folder"
- *
+ * @param string|null $var supported options include "default", "title", "date", and "folder"
* @return string supported options include "default", "title", "date", and "folder"
* @deprecated 1.6
*/
@@ -334,8 +323,7 @@ interface PageLegacyInterface
/**
* Gets the manual order set in the header.
*
- * @param string $var supported options include "default", "title", "date", and "folder"
- *
+ * @param string|null $var supported options include "default", "title", "date", and "folder"
* @return array
* @deprecated 1.6
*/
@@ -345,8 +333,7 @@ interface PageLegacyInterface
* Gets and sets the maxCount field which describes how many sub-pages should be displayed if the
* sub_pages header property is set for this page object.
*
- * @param int $var the maximum number of sub-pages
- *
+ * @param int|null $var the maximum number of sub-pages
* @return int the maximum number of sub-pages
* @deprecated 1.6
*/
@@ -355,9 +342,9 @@ interface PageLegacyInterface
/**
* Gets and sets the modular var that helps identify this page is a modular child
*
- * @param bool $var true if modular_twig
- *
+ * @param bool|null $var true if modular_twig
* @return bool true if modular_twig
+ * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead.
*/
public function modular($var = null);
@@ -365,8 +352,7 @@ interface PageLegacyInterface
* Gets and sets the modular_twig var that helps identify this page as a modular child page that will need
* twig processing handled differently from a regular page.
*
- * @param bool $var true if modular_twig
- *
+ * @param bool|null $var true if modular_twig
* @return bool true if modular_twig
*/
public function modularTwig($var = null);
@@ -374,7 +360,7 @@ interface PageLegacyInterface
/**
* Returns children of this page.
*
- * @return \Grav\Common\Page\Collection
+ * @return PageCollectionInterface|Collection
*/
public function children();
@@ -410,16 +396,14 @@ interface PageLegacyInterface
* Returns the adjacent sibling based on a direction.
*
* @param int $direction either -1 or +1
- *
- * @return PageInterface|bool the sibling page
+ * @return PageInterface|false the sibling page
*/
public function adjacentSibling($direction = 1);
/**
* Helper method to return an ancestor page.
*
- * @param bool $lookup Name of the parent folder
- *
+ * @param bool|null $lookup Name of the parent folder
* @return PageInterface page you were looking for if it exists
*/
public function ancestor($lookup = null);
@@ -429,7 +413,6 @@ interface PageLegacyInterface
* page object is returned.
*
* @param string $field Name of the parent folder
- *
* @return PageInterface
*/
public function inherited($field);
@@ -439,7 +422,6 @@ interface PageLegacyInterface
* first occurrence of an ancestor field will be returned if at all.
*
* @param string $field Name of the parent folder
- *
* @return array
*/
public function inheritedField($field);
@@ -449,7 +431,6 @@ interface PageLegacyInterface
*
* @param string $url the url of the page
* @param bool $all
- *
* @return PageInterface page you were looking for if it exists
*/
public function find($url, $all = false);
@@ -459,17 +440,15 @@ interface PageLegacyInterface
*
* @param string|array $params
* @param bool $pagination
- *
* @return Collection
- * @throws \InvalidArgumentException
+ * @throws InvalidArgumentException
*/
public function collection($params = 'content', $pagination = true);
/**
* @param string|array $value
* @param bool $only_published
- * @return mixed
- * @internal
+ * @return PageCollectionInterface|Collection
*/
public function evaluate($value, $only_published = true);
diff --git a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
index c3afeab1..29002660 100644
--- a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
+++ b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php
@@ -1,6 +1,10 @@
page = $page ?? Grav::instance()['page'] ?? null;
@@ -42,16 +59,26 @@ class Excerpts
$this->config = $config;
}
- public function getPage(): PageInterface
+ /**
+ * @return PageInterface|null
+ */
+ public function getPage(): ?PageInterface
{
return $this->page;
}
+ /**
+ * @return array
+ */
public function getConfig(): array
{
return $this->config;
}
+ /**
+ * @param object $markdown
+ * @return void
+ */
public function fireInitializedEvent($markdown): void
{
$grav = Grav::instance();
@@ -68,8 +95,8 @@ class Excerpts
*/
public function processLinkExcerpt(array $excerpt, string $type = 'link'): array
{
+ $grav = Grav::instance();
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
-
$url_parts = $this->parseUrl($url);
// If there is a query, then parse it and build action calls.
@@ -87,14 +114,18 @@ class Excerpts
);
// Valid attributes supported.
- $valid_attributes = Grav::instance()['config']->get('system.pages.markdown.valid_link_attributes');
+ $valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes') ?? [];
+ $skip = [];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
+ $skip = is_bool($actions['noprocess']) ? $actions : explode(',', $actions['noprocess']);
unset($actions['noprocess']);
- } else {
- // Loop through actions for the image and call them.
- foreach ($actions as $attrib => $value) {
+ }
+
+ // Loop through actions for the image and call them.
+ foreach ($actions as $attrib => $value) {
+ if (!in_array($attrib, $skip)) {
$key = $attrib;
if (in_array($attrib, $valid_attributes, true)) {
@@ -108,12 +139,12 @@ class Excerpts
}
}
- $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
+ $url_parts['query'] = http_build_query($actions, '', '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
- unset ($url_parts['query']);
+ unset($url_parts['query']);
}
// Set path to / if not set.
@@ -124,9 +155,11 @@ class Excerpts
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
- if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
- $grav = Grav::instance();
- $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
+ /** @var UniformResourceLocator $locator */
+ $locator = $grav['locator'];
+ if ($type === 'link' && $locator->isStream($url)) {
+ $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
+ $url_parts['path'] = $grav['base_url_relative'] . '/' . $path;
unset($url_parts['stream'], $url_parts['scheme']);
}
@@ -162,9 +195,10 @@ class Excerpts
$filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
$media = $this->page->getMedia();
-
} else {
$grav = Grav::instance();
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
@@ -181,11 +215,10 @@ class Excerpts
$media = $this->page->getMedia();
} else {
// see if this is an external page to this one
- $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
+ $base_url = rtrim($grav['base_url_relative'] . $pages->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
- /** @var PageInterface $ext_page */
- $ext_page = $grav['pages']->dispatch($page_route, true);
+ $ext_page = $pages->find($page_route, true);
if ($ext_page) {
$media = $ext_page->getMedia();
} else {
@@ -211,7 +244,6 @@ class Excerpts
$id = $element_excerpt['id'] ?? '';
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
-
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
@@ -248,18 +280,15 @@ class Excerpts
);
}
- $config = $this->getConfig();
- if (!empty($config['images']['auto_fix_orientation'])) {
- $actions[] = ['method' => 'fixOrientation', 'params' => ''];
- }
-
- $defaults = $config['images']['defaults'] ?? [];
+ $defaults = $this->config['images']['defaults'] ?? [];
if (count($defaults)) {
foreach ($defaults as $method => $params) {
- $actions[] = [
- 'method' => $method,
- 'params' => $params,
- ];
+ if (array_search($method, array_column($actions, 'method')) === false) {
+ $actions[] = [
+ 'method' => $method,
+ 'params' => $params,
+ ];
+ }
}
}
@@ -287,7 +316,7 @@ class Excerpts
* Variation of parse_url() which works also with local streams.
*
* @param string $url
- * @return array|bool
+ * @return array
*/
protected function parseUrl(string $url)
{
@@ -311,20 +340,4 @@ class Excerpts
return $url_parts;
}
-
- /**
- * @param string $url
- * @return bool|string
- */
- protected function resolveStream(string $url)
- {
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
-
- if ($locator->isStream($url)) {
- return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
- }
-
- return $url;
- }
}
diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php
index 88b2cede..94fd2c4f 100644
--- a/system/src/Grav/Common/Page/Media.php
+++ b/system/src/Grav/Common/Page/Media.php
@@ -3,29 +3,38 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use FilesystemIterator;
use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Common\Yaml;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\GlobalMedia;
use Grav\Common\Page\Medium\MediumFactory;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function in_array;
+/**
+ * Class Media
+ * @package Grav\Common\Page
+ */
class Media extends AbstractMedia
{
+ /** @var GlobalMedia */
protected static $global;
+ /** @var array */
protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];
/**
* @param string $path
- * @param array $media_order
+ * @param array|null $media_order
* @param bool $load
*/
public function __construct($path, array $media_order = null, $load = true)
@@ -44,15 +53,14 @@ class Media extends AbstractMedia
*/
public function __wakeup()
{
- if (!isset(static::$global)) {
+ if (null === static::$global) {
// Add fallback to global media.
- static::$global = new GlobalMedia();
+ static::$global = GlobalMedia::getInstance();
}
}
/**
- * @param mixed $offset
- *
+ * @param string $offset
* @return bool
*/
public function offsetExists($offset)
@@ -61,9 +69,8 @@ class Media extends AbstractMedia
}
/**
- * @param mixed $offset
- *
- * @return mixed
+ * @param string $offset
+ * @return MediaObjectInterface|null
*/
public function offsetGet($offset)
{
@@ -72,58 +79,65 @@ class Media extends AbstractMedia
/**
* Initialize class.
+ *
+ * @return void
*/
protected function init()
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$config = Grav::instance()['config'];
- $locator = Grav::instance()['locator'];
$exif_reader = isset(Grav::instance()['exif']) ? Grav::instance()['exif']->getReader() : false;
$media_types = array_keys(Grav::instance()['config']->get('media.types'));
+ $path = $this->getPath();
// Handle special cases where page doesn't exist in filesystem.
- if (!is_dir($this->getPath())) {
+ if (!$path || !is_dir($path)) {
return;
}
- $iterator = new \FilesystemIterator($this->getPath(), \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
+ $iterator = new FilesystemIterator($path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
$media = [];
- /** @var \DirectoryIterator $info */
- foreach ($iterator as $path => $info) {
+ foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files.
- if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
+ $filename = $info->getFilename();
+ if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
continue;
}
// Find out what type we're dealing with
- list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename());
+ [$basename, $ext, $type, $extra] = $this->getFileParts($filename);
- if (!\in_array(strtolower($ext), $media_types, true)) {
+ if (!in_array(strtolower($ext), $media_types, true)) {
continue;
}
if ($type === 'alternative') {
- $media["{$basename}.{$ext}"][$type][$extra] = ['file' => $path, 'size' => $info->getSize()];
+ $media["{$basename}.{$ext}"][$type][$extra] = ['file' => $file, 'size' => $info->getSize()];
} else {
- $media["{$basename}.{$ext}"][$type] = ['file' => $path, 'size' => $info->getSize()];
+ $media["{$basename}.{$ext}"][$type] = ['file' => $file, 'size' => $info->getSize()];
}
}
foreach ($media as $name => $types) {
// First prepare the alternatives in case there is no base medium
if (!empty($types['alternative'])) {
+ /**
+ * @var string|int $ratio
+ * @var array $alt
+ */
foreach ($types['alternative'] as $ratio => &$alt) {
- $alt['file'] = MediumFactory::fromFile($alt['file']);
+ $alt['file'] = $this->createFromFile($alt['file']);
- if (!$alt['file']) {
+ if (empty($alt['file'])) {
unset($types['alternative'][$ratio]);
} else {
$alt['file']->set('size', $alt['size']);
}
}
+ unset($alt);
}
$file_path = null;
@@ -139,9 +153,11 @@ class Media extends AbstractMedia
$file_path = $medium->path();
$medium = MediumFactory::scaledFromMedium($medium, $max, 1)['file'];
} else {
- $medium = MediumFactory::fromFile($types['base']['file']);
- $medium && $medium->set('size', $types['base']['size']);
- $file_path = $medium->path();
+ $medium = $this->createFromFile($types['base']['file']);
+ if ($medium) {
+ $medium->set('size', $types['base']['size']);
+ $file_path = $medium->path();
+ }
}
if (empty($medium)) {
@@ -154,7 +170,6 @@ class Media extends AbstractMedia
if (file_exists($meta_path)) {
$types['meta']['file'] = $meta_path;
} elseif ($file_path && $exif_reader && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) {
-
$meta = $exif_reader->read($file_path);
if ($meta) {
@@ -212,10 +227,10 @@ class Media extends AbstractMedia
}
/**
- * @return string
+ * @return string|null
* @deprecated 1.6 Use $this->getPath() instead.
*/
- public function path()
+ public function path(): ?string
{
return $this->getPath();
}
diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
index f67cd650..db81ddd4 100644
--- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php
+++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php
@@ -3,49 +3,72 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+use Grav\Common\Config\Config;
+use Grav\Common\Data\Blueprint;
use Grav\Common\Grav;
+use Grav\Common\Language\Language;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
-use Grav\Common\Page\Page;
+use Grav\Common\Media\Interfaces\MediaUploadInterface;
+use Grav\Common\Media\Traits\MediaUploadTrait;
+use Grav\Common\Page\Pages;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\ArrayTraits\Iterator;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function is_array;
-abstract class AbstractMedia implements ExportInterface, MediaCollectionInterface
+/**
+ * Class AbstractMedia
+ * @package Grav\Common\Page\Medium
+ */
+abstract class AbstractMedia implements ExportInterface, MediaCollectionInterface, MediaUploadInterface
{
use ArrayAccess;
use Countable;
use Iterator;
use Export;
+ use MediaUploadTrait;
+ /** @var array */
protected $items = [];
+ /** @var string|null */
protected $path;
+ /** @var array */
protected $images = [];
+ /** @var array */
protected $videos = [];
+ /** @var array */
protected $audios = [];
+ /** @var array */
protected $files = [];
+ /** @var array|null */
protected $media_order;
/**
* Return media path.
*
- * @return string
+ * @return string|null
*/
- public function getPath()
+ public function getPath(): ?string
{
return $this->path;
}
- public function setPath(?string $path)
+ /**
+ * @param string|null $path
+ * @return void
+ */
+ public function setPath(?string $path): void
{
$this->path = $path;
}
@@ -54,7 +77,7 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
* Get medium by filename.
*
* @param string $filename
- * @return Medium|null
+ * @return MediaObjectInterface|null
*/
public function get($filename)
{
@@ -80,7 +103,6 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
*/
public function setTimestamps($timestamp = null)
{
- /** @var Medium $instance */
foreach ($this->items as $instance) {
$instance->setTimestamp($timestamp);
}
@@ -150,14 +172,17 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
/**
* @param string $name
- * @param MediaObjectInterface $file
+ * @param MediaObjectInterface|null $file
+ * @return void
*/
public function add($name, $file)
{
- if (!$file) {
+ if (null === $file) {
return;
}
+
$this->offsetSet($name, $file);
+
switch ($file->type) {
case 'image':
$this->images[$name] = $file;
@@ -173,6 +198,50 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
}
}
+ /**
+ * @param string $name
+ * @return void
+ */
+ public function hide($name)
+ {
+ $this->offsetUnset($name);
+
+ unset($this->images[$name], $this->videos[$name], $this->audios[$name], $this->files[$name]);
+ }
+
+ /**
+ * Create Medium from a file.
+ *
+ * @param string $file
+ * @param array $params
+ * @return Medium|null
+ */
+ public function createFromFile($file, array $params = [])
+ {
+ return MediumFactory::fromFile($file, $params);
+ }
+
+ /**
+ * Create Medium from array of parameters
+ *
+ * @param array $items
+ * @param Blueprint|null $blueprint
+ * @return Medium|null
+ */
+ public function createFromArray(array $items = [], Blueprint $blueprint = null)
+ {
+ return MediumFactory::fromArray($items, $blueprint);
+ }
+
+ /**
+ * @param MediaObjectInterface $mediaObject
+ * @return ImageFile
+ */
+ public function getImageFileObject(MediaObjectInterface $mediaObject): ImageFile
+ {
+ return ImageFile::open($mediaObject->get('filepath'));
+ }
+
/**
* Order the media based on the page's media_order
*
@@ -182,11 +251,14 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
protected function orderMedia($media)
{
if (null === $this->media_order) {
- /** @var Page $page */
- $page = Grav::instance()['pages']->get($this->getPath());
-
- if ($page && isset($page->header()->media_order)) {
- $this->media_order = array_map('trim', explode(',', $page->header()->media_order));
+ $path = $this->getPath();
+ if (null !== $path) {
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
+ $page = $pages->get($path);
+ if ($page && isset($page->header()->media_order)) {
+ $this->media_order = array_map('trim', explode(',', $page->header()->media_order));
+ }
}
}
@@ -199,6 +271,11 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
return $media;
}
+ protected function fileExists(string $filename, string $destination): bool
+ {
+ return file_exists("{$destination}/{$filename}");
+ }
+
/**
* Get filename, extension and meta part.
*
@@ -239,6 +316,28 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
}
}
- return array($name, $extension, $type, $extra);
+ return [$name, $extension, $type, $extra];
+ }
+
+ protected function getGrav(): Grav
+ {
+ return Grav::instance();
+ }
+
+ protected function getConfig(): Config
+ {
+ return $this->getGrav()['config'];
+ }
+
+ protected function getLanguage(): Language
+ {
+ return $this->getGrav()['language'];
+ }
+
+ protected function clearCache(): void
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->getGrav()['locator'];
+ $locator->clearCache();
}
}
diff --git a/system/src/Grav/Common/Page/Medium/AudioMedium.php b/system/src/Grav/Common/Page/Medium/AudioMedium.php
index 74ef746a..f34f0a9c 100644
--- a/system/src/Grav/Common/Page/Medium/AudioMedium.php
+++ b/system/src/Grav/Common/Page/Medium/AudioMedium.php
@@ -3,134 +3,22 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
-class AudioMedium extends Medium
-{
- use StaticResizeTrait;
-
- /**
- * Parsedown element for source display mode
- *
- * @param array $attributes
- * @param bool $reset
- * @return array
- */
- protected function sourceParsedownElement(array $attributes, $reset = true)
- {
- $location = $this->url($reset);
-
- return [
- 'name' => 'audio',
- 'text' => 'Your browser does not support the audio tag.',
- 'attributes' => $attributes
- ];
- }
-
- /**
- * Allows to set or remove the HTML5 default controls
- *
- * @param bool $display
- * @return $this
- */
- public function controls($display = true)
- {
- if($display) {
- $this->attributes['controls'] = true;
- } else {
- unset($this->attributes['controls']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the preload behaviour
- *
- * @param string $preload
- * @return $this
- */
- public function preload($preload)
- {
- $validPreloadAttrs = ['auto', 'metadata', 'none'];
-
- if (\in_array($preload, $validPreloadAttrs, true)) {
- $this->attributes['preload'] = $preload;
- }
-
- return $this;
- }
-
- /**
- * Allows to set the controlsList behaviour
- * Separate multiple values with a hyphen
- *
- * @param string $controlsList
- * @return $this
- */
- public function controlsList($controlsList)
- {
- $controlsList = str_replace('-', ' ', $controlsList);
- $this->attributes['controlsList'] = $controlsList;
-
- return $this;
- }
-
- /**
- * Allows to set the muted attribute
- *
- * @param bool $status
- * @return $this
- */
- public function muted($status = false)
- {
- if($status) {
- $this->attributes['muted'] = true;
- } else {
- unset($this->attributes['muted']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the loop attribute
- *
- * @param bool $status
- * @return $this
- */
- public function loop($status = false)
- {
- if($status) {
- $this->attributes['loop'] = true;
- } else {
- unset($this->attributes['loop']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the autoplay attribute
- *
- * @param bool $status
- * @return $this
- */
- public function autoplay($status = false)
- {
- if($status) {
- $this->attributes['autoplay'] = true;
- } else {
- unset($this->attributes['autoplay']);
- }
-
- return $this;
- }
+use Grav\Common\Media\Interfaces\AudioMediaInterface;
+use Grav\Common\Media\Traits\AudioMediaTrait;
+/**
+ * Class AudioMedium
+ * @package Grav\Common\Page\Medium
+ */
+class AudioMedium extends Medium implements AudioMediaInterface
+{
+ use AudioMediaTrait;
/**
* Reset medium.
@@ -141,7 +29,7 @@ class AudioMedium extends Medium
{
parent::reset();
- $this->attributes['controls'] = true;
+ $this->resetPlayer();
return $this;
}
diff --git a/system/src/Grav/Common/Page/Medium/GlobalMedia.php b/system/src/Grav/Common/Page/Medium/GlobalMedia.php
index 9fb30e98..4097c8f5 100644
--- a/system/src/Grav/Common/Page/Medium/GlobalMedia.php
+++ b/system/src/Grav/Common/Page/Medium/GlobalMedia.php
@@ -3,30 +3,47 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function dirname;
+/**
+ * Class GlobalMedia
+ * @package Grav\Common\Page\Medium
+ */
class GlobalMedia extends AbstractMedia
{
+ /** @var self */
+ protected static $instance;
+
+ public static function getInstance(): self
+ {
+ if (null === self::$instance) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
/**
* Return media path.
*
- * @return null
+ * @return string|null
*/
- public function getPath()
+ public function getPath(): ?string
{
return null;
}
/**
- * @param mixed $offset
- *
+ * @param string $offset
* @return bool
*/
public function offsetExists($offset)
@@ -35,9 +52,8 @@ class GlobalMedia extends AbstractMedia
}
/**
- * @param mixed $offset
- *
- * @return mixed
+ * @param string $offset
+ * @return MediaObjectInterface|null
*/
public function offsetGet($offset)
{
@@ -52,13 +68,16 @@ class GlobalMedia extends AbstractMedia
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
+ if (!$locator->isStream($filename)) {
+ return null;
+ }
- return $locator->isStream($filename) ? ($locator->findResource($filename) ?: null) : null;
+ return $locator->findResource($filename) ?: null;
}
/**
* @param string $stream
- * @return Medium|null
+ * @return MediaObjectInterface|null
*/
protected function addMedium($stream)
{
@@ -68,10 +87,10 @@ class GlobalMedia extends AbstractMedia
}
$path = dirname($filename);
- list($basename, $ext,, $extra) = $this->getFileParts(basename($filename));
+ [$basename, $ext,, $extra] = $this->getFileParts(basename($filename));
$medium = MediumFactory::fromFile($filename);
- if (empty($medium)) {
+ if (null === $medium) {
return null;
}
diff --git a/system/src/Grav/Common/Page/Medium/ImageFile.php b/system/src/Grav/Common/Page/Medium/ImageFile.php
index d56c3422..bd853bfc 100644
--- a/system/src/Grav/Common/Page/Medium/ImageFile.php
+++ b/system/src/Grav/Common/Page/Medium/ImageFile.php
@@ -3,27 +3,49 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+use Exception;
+use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Gregwar\Image\Exceptions\GenerationError;
use Gregwar\Image\Image;
use Gregwar\Image\Source;
use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use function array_key_exists;
+use function count;
+use function extension_loaded;
+use function in_array;
+/**
+ * Class ImageFile
+ * @package Grav\Common\Page\Medium
+ *
+ * @method Image applyExifOrientation($exif_orienation)
+ */
class ImageFile extends Image
{
+ /**
+ * Destruct also image object.
+ */
public function __destruct()
{
- $this->getAdapter()->deinit();
+ $adapter = $this->adapter;
+ if ($adapter) {
+ $adapter->deinit();
+ }
}
/**
* Clear previously applied operations
+ *
+ * @return void
*/
public function clearOperations()
{
@@ -37,7 +59,6 @@ class ImageFile extends Image
* @param int $quality the quality (for JPEG)
* @param bool $actual
* @param array $extras
- *
* @return string
*/
public function cacheFile($type = 'jpg', $quality = 80, $actual = false, $extras = [])
@@ -53,8 +74,11 @@ class ImageFile extends Image
// Computes the hash
$this->hash = $this->getHash($type, $quality, $extras);
+ /** @var Config $config */
+ $config = Grav::instance()['config'];
+
// Seo friendly image names
- $seofriendly = Grav::instance()['config']->get('system.images.seofriendly', false);
+ $seofriendly = $config->get('system.images.seofriendly', false);
if ($seofriendly) {
$mini_hash = substr($this->hash, 0, 4) . substr($this->hash, -4);
@@ -87,7 +111,7 @@ class ImageFile extends Image
// Asking the cache for the cacheFile
try {
- $perms = Grav::instance()['config']->get('system.images.cache_perms', '0755');
+ $perms = $config->get('system.images.cache_perms', '0755');
$perms = octdec($perms);
$file = $this->getCacheSystem()->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
} catch (GenerationError $e) {
@@ -95,8 +119,9 @@ class ImageFile extends Image
}
// Nulling the resource
- $this->getAdapter()->setSource(new Source\File($file));
- $this->getAdapter()->deinit();
+ $adapter = $this->getAdapter();
+ $adapter->setSource(new Source\File($file));
+ $adapter->deinit();
if ($actual) {
return $file;
@@ -107,10 +132,11 @@ class ImageFile extends Image
/**
* Gets the hash.
+ *
* @param string $type
* @param int $quality
- * @param [] $extras
- * @return null
+ * @param array $extras
+ * @return string
*/
public function getHash($type = 'guess', $quality = 80, $extras = [])
{
@@ -123,6 +149,7 @@ class ImageFile extends Image
/**
* Generates the hash.
+ *
* @param string $type
* @param int $quality
* @param array $extras
@@ -131,15 +158,54 @@ class ImageFile extends Image
{
$inputInfos = $this->source->getInfos();
- $datas = array(
+ $data = [
$inputInfos,
$this->serializeOperations(),
$type,
$quality,
$extras
- );
+ ];
- $this->hash = sha1(serialize($datas));
+ $this->hash = sha1(serialize($data));
}
+ /**
+ * Read exif rotation from file and apply it.
+ */
+ public function fixOrientation()
+ {
+ if (!extension_loaded('exif')) {
+ throw new RuntimeException('You need to EXIF PHP Extension to use this function');
+ }
+
+ if (!in_array(exif_imagetype($this->source->getInfos()), [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM], true)) {
+ return $this;
+ }
+
+ // resolve any streams
+ /** @var UniformResourceLocator $locator */
+ $locator = Grav::instance()['locator'];
+ $filepath = $this->source->getInfos();
+ if ($locator->isStream($filepath)) {
+ $filepath = $locator->findResource($this->source->getInfos(), true, true);
+ }
+
+ // Make sure file exists
+ if (!file_exists($filepath)) {
+ return $this;
+ }
+
+ try {
+ $exif = @exif_read_data($filepath);
+ } catch (Exception $e) {
+ Grav::instance()['log']->error($filepath . ' - ' . $e->getMessage());
+ return $this;
+ }
+
+ if ($exif === false || !array_key_exists('Orientation', $exif)) {
+ return $this;
+ }
+
+ return $this->applyExifOrientation($exif['Orientation']);
+ }
}
diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php
index 13c142ab..70d13f8f 100644
--- a/system/src/Grav/Common/Page/Medium/ImageMedium.php
+++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php
@@ -3,102 +3,69 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+use BadFunctionCallException;
use Grav\Common\Data\Blueprint;
-use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\ImageManipulateInterface;
+use Grav\Common\Media\Interfaces\ImageMediaInterface;
+use Grav\Common\Media\Interfaces\MediaLinkInterface;
+use Grav\Common\Media\Traits\ImageLoadingTrait;
+use Grav\Common\Media\Traits\ImageMediaTrait;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function func_get_args;
+use function in_array;
-class ImageMedium extends Medium
+/**
+ * Class ImageMedium
+ * @package Grav\Common\Page\Medium
+ */
+class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulateInterface
{
- /**
- * @var array
- */
- protected $thumbnailTypes = ['page', 'media', 'default'];
-
- /**
- * @var ImageFile
- */
- protected $image;
-
- /**
- * @var string
- */
- protected $format = 'guess';
-
- /**
- * @var int
- */
- protected $quality;
-
- /**
- * @var int
- */
- protected $default_quality;
+ use ImageMediaTrait;
+ use ImageLoadingTrait;
/**
- * @var bool
+ * @var mixed|string
*/
- protected $debug_watermarked = false;
-
- /**
- * @var array
- */
- public static $magic_actions = [
- 'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
- 'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
- 'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
- 'rotate', 'flip', 'fixOrientation', 'gaussianBlur'
- ];
-
- /**
- * @var array
- */
- public static $magic_resize_actions = [
- 'resize' => [0, 1],
- 'forceResize' => [0, 1],
- 'cropResize' => [0, 1],
- 'crop' => [0, 1, 2, 3],
- 'zoomCrop' => [0, 1]
- ];
-
- /**
- * @var string
- */
- protected $sizes = '100vw';
+ private $saved_image_path;
/**
* Construct.
*
* @param array $items
- * @param Blueprint $blueprint
+ * @param Blueprint|null $blueprint
*/
public function __construct($items = [], Blueprint $blueprint = null)
{
parent::__construct($items, $blueprint);
- $config = Grav::instance()['config'];
+ $config = $this->getGrav()['config'];
+
+ $this->thumbnailTypes = ['page', 'media', 'default'];
+ $this->default_quality = $config->get('system.images.default_image_quality', 85);
+ $this->def('debug', $config->get('system.images.debug'));
$path = $this->get('filepath');
if (!$path || !file_exists($path) || !filesize($path)) {
return;
}
- $image_info = getimagesize($path);
+ $this->set('thumbnails.media', $path);
- $this->def('width', $image_info[0]);
- $this->def('height', $image_info[1]);
- $this->def('mime', $image_info['mime']);
- $this->def('debug', $config->get('system.images.debug'));
-
- $this->set('thumbnails.media', $this->get('filepath'));
-
- $this->default_quality = $config->get('system.images.default_image_quality', 85);
+ if (!($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) {
+ $image_info = getimagesize($path);
+ if ($image_info) {
+ $this->def('width', $image_info[0]);
+ $this->def('height', $image_info[1]);
+ $this->def('mime', $image_info['mime']);
+ }
+ }
$this->reset();
@@ -107,18 +74,67 @@ class ImageMedium extends Medium
}
}
+ /**
+ * @return array
+ */
+ public function getMeta(): array
+ {
+ return [
+ 'width' => $this->width,
+ 'height' => $this->height,
+ ] + parent::getMeta();
+ }
+
+ /**
+ * Also unset the image on destruct.
+ */
public function __destruct()
{
unset($this->image);
}
+ /**
+ * Also clone image.
+ */
public function __clone()
{
- $this->image = $this->image ? clone $this->image : null;
+ if ($this->image) {
+ $this->image = clone $this->image;
+ }
parent::__clone();
}
+ /**
+ * Reset image.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ parent::reset();
+
+ if ($this->image) {
+ $this->image();
+ $this->medium_querystring = [];
+ $this->filter();
+ $this->clearAlternatives();
+ }
+
+ $this->format = 'guess';
+ $this->quality = $this->default_quality;
+
+ $this->debug_watermarked = false;
+
+ $config = $this->getGrav()['config'];
+ // Set CLS configuration
+ $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false);
+ $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false);
+ $this->retina_scale = $config->get('system.images.cls.retina_scale', 1);
+
+ return $this;
+ }
+
/**
* Add meta file for the medium.
*
@@ -135,14 +151,6 @@ class ImageMedium extends Medium
return $this;
}
- /**
- * Clear out the alternatives
- */
- public function clearAlternatives()
- {
- $this->alternatives = [];
- }
-
/**
* Return PATH to image.
*
@@ -168,15 +176,17 @@ class ImageMedium extends Medium
*/
public function url($reset = true)
{
+ $grav = $this->getGrav();
+
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
- $saved_image_path = $this->saveImage();
+ $locator = $grav['locator'];
+ $image_path = (string)($locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true));
+ $saved_image_path = $this->saved_image_path = $this->saveImage();
- $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
+ $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path) ?: $saved_image_path;
if ($locator->isStream($output)) {
- $output = $locator->findResource($output, false);
+ $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true));
}
if (Utils::startsWith($output, $image_path)) {
@@ -188,24 +198,9 @@ class ImageMedium extends Medium
$this->reset();
}
- return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
- }
-
- /**
- * Simply processes with no extra methods. Useful for triggering events.
- *
- * @return $this
- */
- public function cache()
- {
- if (!$this->image) {
- $this->image();
- }
-
- return $this;
+ return trim($grav['base_url'] . '/' . $this->urlQuerystring($output), '\\');
}
-
/**
* Return srcset string for this Medium and its alternatives.
*
@@ -231,106 +226,6 @@ class ImageMedium extends Medium
return implode(', ', $srcset);
}
- /**
- * Allows the ability to override the image's pretty name stored in cache
- *
- * @param string $name
- */
- public function setImagePrettyName($name)
- {
- $this->set('prettyname', $name);
- if ($this->image) {
- $this->image->setPrettyName($name);
- }
- }
-
- public function getImagePrettyName()
- {
- if ($this->get('prettyname')) {
- return $this->get('prettyname');
- }
-
- $basename = $this->get('basename');
- if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
- $basename = $matches[1];
- }
- return $basename;
- }
-
- /**
- * Generate alternative image widths, using either an array of integers, or
- * a min width, a max width, and a step parameter to fill out the necessary
- * widths. Existing image alternatives won't be overwritten.
- *
- * @param int|int[] $min_width
- * @param int $max_width
- * @param int $step
- * @return $this
- */
- public function derivatives($min_width, $max_width = 2500, $step = 200)
- {
- if (!empty($this->alternatives)) {
- $max = max(array_keys($this->alternatives));
- $base = $this->alternatives[$max];
- } else {
- $base = $this;
- }
-
- $widths = [];
-
- if (func_num_args() === 1) {
- foreach ((array) func_get_arg(0) as $width) {
- if ($width < $base->get('width')) {
- $widths[] = $width;
- }
- }
- } else {
- $max_width = min($max_width, $base->get('width'));
-
- for ($width = $min_width; $width < $max_width; $width = $width + $step) {
- $widths[] = $width;
- }
- }
-
- foreach ($widths as $width) {
- // Only generate image alternatives that don't already exist
- if (array_key_exists((int) $width, $this->alternatives)) {
- continue;
- }
-
- $derivative = MediumFactory::fromFile($base->get('filepath'));
-
- // It's possible that MediumFactory::fromFile returns null if the
- // original image file no longer exists and this class instance was
- // retrieved from the page cache
- if (null !== $derivative) {
- $index = 2;
- $alt_widths = array_keys($this->alternatives);
- sort($alt_widths);
-
- foreach ($alt_widths as $i => $key) {
- if ($width > $key) {
- $index += max($i, 1);
- }
- }
-
- $basename = preg_replace('/(@\d+x){0,1}$/', "@{$width}w", $base->get('basename'), 1);
- $derivative->setImagePrettyName($basename);
-
- $ratio = $base->get('width') / $width;
- $height = $derivative->get('height') / $ratio;
-
- $derivative->resize($width, $height);
- $derivative->set('width', $width);
- $derivative->set('height', $height);
-
- $this->addAlternative($ratio, $derivative);
- }
- }
-
- return $this;
- }
-
/**
* Parsedown element for source display mode
*
@@ -348,31 +243,24 @@ class ImageMedium extends Medium
$attributes['sizes'] = $this->sizes();
}
- return ['name' => 'img', 'attributes' => $attributes];
- }
+ if ($this->saved_image_path && $this->auto_sizes) {
+ if (!array_key_exists('height', $this->attributes) && !array_key_exists('width', $this->attributes)) {
+ $info = getimagesize($this->saved_image_path);
+ $width = intval($info[0]);
+ $height = intval($info[1]);
- /**
- * Reset image.
- *
- * @return $this
- */
- public function reset()
- {
- parent::reset();
+ $scaling_factor = $this->retina_scale > 0 ? $this->retina_scale : 1;
+ $attributes['width'] = intval($width / $scaling_factor);
+ $attributes['height'] = intval($height / $scaling_factor);
- if ($this->image) {
- $this->image();
- $this->medium_querystring = [];
- $this->filter();
- $this->clearAlternatives();
+ if ($this->aspect_ratio) {
+ $style = ($attributes['style'] ?? ' ') . "--aspect-ratio: $width/$height;";
+ $attributes['style'] = trim($style);
+ }
+ }
}
- $this->format = 'guess';
- $this->quality = $this->default_quality;
-
- $this->debug_watermarked = false;
-
- return $this;
+ return ['name' => 'img', 'attributes' => $attributes];
}
/**
@@ -380,7 +268,7 @@ class ImageMedium extends Medium
*
* @param bool $reset
* @param array $attributes
- * @return Link
+ * @return MediaLinkInterface
*/
public function link($reset = true, array $attributes = [])
{
@@ -399,7 +287,7 @@ class ImageMedium extends Medium
* @param int $width
* @param int $height
* @param bool $reset
- * @return Link
+ * @return MediaLinkInterface
*/
public function lightbox($width = null, $height = null, $reset = true)
{
@@ -414,115 +302,70 @@ class ImageMedium extends Medium
return parent::lightbox($width, $height, $reset);
}
- /**
- * Sets or gets the quality of the image
- *
- * @param int $quality 0-100 quality
- * @return int|$this
- */
- public function quality($quality = null)
+ public function autoSizes($enabled = 'true')
{
- if ($quality) {
- if (!$this->image) {
- $this->image();
- }
-
- $this->quality = $quality;
-
- return $this;
- }
+ $enabled = $enabled === 'true' ?: false;
+ $this->auto_sizes = $enabled;
- return $this->quality;
+ return $this;
}
- /**
- * Sets image output format.
- *
- * @param string $format
- * @return $this
- */
- public function format($format)
+ public function aspectRatio($enabled = 'true')
{
- if (!$this->image) {
- $this->image();
- }
-
- $this->format = $format;
+ $enabled = $enabled === 'true' ?: false;
+ $this->aspect_ratio = $enabled;
return $this;
}
- /**
- * Set or get sizes parameter for srcset media action
- *
- * @param string $sizes
- * @return string
- */
- public function sizes($sizes = null)
+ public function retinaScale($scale = 1)
{
+ $this->retina_scale = intval($scale);
- if ($sizes) {
- $this->sizes = $sizes;
-
- return $this;
- }
-
- return empty($this->sizes) ? '100vw' : $this->sizes;
+ return $this;
}
/**
- * Allows to set the width attribute from Markdown or Twig
- * Examples: 
- * 
- * 
- * 
- * {{ page.media['myimg.png'].width().height().html }}
- * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
+ * Handle this commonly used variant
*
- * @param mixed $value A value or 'auto' or empty to use the width of the image
* @return $this
*/
- public function width($value = 'auto')
+ public function cropZoom()
{
- if (!$value || $value === 'auto') {
- $this->attributes['width'] = $this->get('width');
- } else {
- $this->attributes['width'] = $value;
- }
+ $this->__call('zoomCrop', func_get_args());
return $this;
}
/**
- * Allows to set the height attribute from Markdown or Twig
- * Examples: 
- * 
- * 
- * 
- * {{ page.media['myimg.png'].width().height().html }}
- * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
+ * Add a frame to image
*
- * @param mixed $value A value or 'auto' or empty to use the height of the image
* @return $this
*/
- public function height($value = 'auto')
+ public function addFrame(int $border = 10, string $color = '0x000000')
{
- if (!$value || $value === 'auto') {
- $this->attributes['height'] = $this->get('height');
- } else {
- $this->attributes['height'] = $value;
- }
-
+ if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
+ $image = ImageFile::open($this->path());
+ }
+ else {
return $this;
- }
+ }
+
+ $dst_width = $image->width()+2*$border;
+ $dst_height = $image->height()+2*$border;
+
+ $frame = ImageFile::create($dst_width, $dst_height);
+
+ $frame->__call('fill', [$color]);
+
+ $this->image = $frame;
+
+ $this->__call('merge', [$image, $border, $border]);
+
+ $this->saveImage();
+
+ return $this;
- /**
- * Handle this commonly used variant
- */
- public function cropZoom()
- {
- $this->__call('zoomCrop', func_get_args());
- return $this;
}
/**
@@ -532,9 +375,10 @@ class ImageMedium extends Medium
* @param mixed $args
* @return $this|mixed
*/
+
public function __call($method, $args)
{
- if (!\in_array($method, self::$magic_actions, true)) {
+ if (!in_array($method, static::$magic_actions, true)) {
return parent::__call($method, $args);
}
@@ -544,125 +388,28 @@ class ImageMedium extends Medium
}
try {
- call_user_func_array([$this->image, $method], $args);
+ $this->image->{$method}(...$args);
+ /** @var ImageMediaInterface $medium */
foreach ($this->alternatives as $medium) {
- if (!$medium->image) {
- $medium->image();
- }
-
$args_copy = $args;
// regular image: resize 400x400 -> 200x200
// --> @2x: resize 800x800->400x400
- if (isset(self::$magic_resize_actions[$method])) {
- foreach (self::$magic_resize_actions[$method] as $param) {
+ if (isset(static::$magic_resize_actions[$method])) {
+ foreach (static::$magic_resize_actions[$method] as $param) {
if (isset($args_copy[$param])) {
$args_copy[$param] *= $medium->get('ratio');
}
}
}
- call_user_func_array([$medium, $method], $args_copy);
- }
- } catch (\BadFunctionCallException $e) {
- }
-
- return $this;
- }
-
- /**
- * Gets medium image, resets image manipulation operations.
- *
- * @return $this
- */
- protected function image()
- {
- $locator = Grav::instance()['locator'];
-
- $file = $this->get('filepath');
-
- // Use existing cache folder or if it doesn't exist, create it.
- $cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
-
- // Make sure we free previous image.
- unset($this->image);
-
- $this->image = ImageFile::open($file)
- ->setCacheDir($cacheDir)
- ->setActualCacheDir($cacheDir)
- ->setPrettyName($this->getImagePrettyName());
-
- return $this;
- }
-
- /**
- * Save the image with cache.
- *
- * @return string
- */
- protected function saveImage()
- {
- if (!$this->image) {
- return parent::path(false);
- }
-
- $this->filter();
-
- if (isset($this->result)) {
- return $this->result;
- }
-
- if (!$this->debug_watermarked && $this->get('debug')) {
- $ratio = $this->get('ratio');
- if (!$ratio) {
- $ratio = 1;
- }
-
- $locator = Grav::instance()['locator'];
- $overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
- $this->image->merge(ImageFile::open($overlay));
- }
-
- return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
- }
-
- /**
- * Filter image by using user defined filter parameters.
- *
- * @param string $filter Filter to be used.
- */
- public function filter($filter = 'image.filters.default')
- {
- $filters = (array) $this->get($filter, []);
- foreach ($filters as $params) {
- $params = (array) $params;
- $method = array_shift($params);
- $this->__call($method, $params);
- }
- }
-
- /**
- * Return the image higher quality version
- *
- * @return ImageMedium the alternative version with higher quality
- */
- public function higherQualityAlternative()
- {
- if ($this->alternatives) {
- $max = reset($this->alternatives);
- foreach($this->alternatives as $alternative)
- {
- if($alternative->quality() > $max->quality())
- {
- $max = $alternative;
- }
+ // Do the same call for alternative media.
+ $medium->__call($method, $args_copy);
}
-
- return $max;
+ } catch (BadFunctionCallException $e) {
}
return $this;
}
-
}
diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php
index 69786351..d5768757 100644
--- a/system/src/Grav/Common/Page/Medium/Link.php
+++ b/system/src/Grav/Common/Page/Medium/Link.php
@@ -3,41 +3,60 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
-class Link implements RenderableInterface
+use BadMethodCallException;
+use Grav\Common\Media\Interfaces\MediaLinkInterface;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
+use RuntimeException;
+use function call_user_func_array;
+use function get_class;
+use function is_array;
+use function is_callable;
+
+/**
+ * Class Link
+ * @package Grav\Common\Page\Medium
+ */
+class Link implements RenderableInterface, MediaLinkInterface
{
use ParsedownHtmlTrait;
- /**
- * @var array
- */
+ /** @var array */
protected $attributes = [];
+ /** @var MediaObjectInterface */
protected $source;
/**
* Construct.
* @param array $attributes
- * @param Medium $medium
+ * @param MediaObjectInterface $medium
*/
- public function __construct(array $attributes, Medium $medium)
+ public function __construct(array $attributes, MediaObjectInterface $medium)
{
$this->attributes = $attributes;
- $this->source = $medium->reset()->thumbnail('auto')->display('thumbnail');
- $this->source->linked = true;
+
+ $source = $medium->reset()->thumbnail('auto')->display('thumbnail');
+ if (!$source instanceof MediaObjectInterface) {
+ throw new RuntimeException('Media has no thumbnail set');
+ }
+
+ $source->set('linked', true);
+
+ $this->source = $source;
}
/**
* Get an element (is array) that can be rendered by the Parsedown engine
*
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
* @param bool $reset
* @return array
*/
@@ -48,7 +67,7 @@ class Link implements RenderableInterface
return [
'name' => 'a',
'attributes' => $this->attributes,
- 'handler' => is_string($innerElement) ? 'line' : 'element',
+ 'handler' => is_array($innerElement) ? 'element' : 'line',
'text' => $innerElement
];
}
@@ -62,10 +81,21 @@ class Link implements RenderableInterface
*/
public function __call($method, $args)
{
- $this->source = call_user_func_array(array($this->source, $method), $args);
+ $object = $this->source;
+ $callable = [$object, $method];
+ if (!is_callable($callable)) {
+ throw new BadMethodCallException(get_class($object) . '::' . $method . '() not found.');
+ }
+
+ $object = call_user_func_array($callable, $args);
+ if (!$object instanceof MediaLinkInterface) {
+ // Don't start nesting links, if user has multiple link calls in his
+ // actions, we will drop the previous links.
+ return $this;
+ }
+
+ $this->source = $object;
- // Don't start nesting links, if user has multiple link calls in his
- // actions, we will drop the previous links.
- return $this->source instanceof Link ? $this->source : $this;
+ return $object;
}
}
diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php
index bf0bb1fa..87009ac8 100644
--- a/system/src/Grav/Common/Page/Medium/Medium.php
+++ b/system/src/Grav/Common/Page/Medium/Medium.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,10 @@ use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Common\Data\Blueprint;
-use Grav\Common\Media\Interfaces\MediaObjectInterface;
-use Grav\Common\Utils;
+use Grav\Common\Media\Interfaces\MediaFileInterface;
+use Grav\Common\Media\Interfaces\MediaLinkInterface;
+use Grav\Common\Media\Traits\MediaFileTrait;
+use Grav\Common\Media\Traits\MediaObjectTrait;
/**
* Class Medium
@@ -22,59 +24,17 @@ use Grav\Common\Utils;
*
* @property string $mime
*/
-class Medium extends Data implements RenderableInterface, MediaObjectInterface
+class Medium extends Data implements RenderableInterface, MediaFileInterface
{
+ use MediaObjectTrait;
+ use MediaFileTrait;
use ParsedownHtmlTrait;
- /**
- * @var string
- */
- protected $mode = 'source';
-
- /**
- * @var Medium
- */
- protected $_thumbnail = null;
-
- /**
- * @var array
- */
- protected $thumbnailTypes = ['page', 'default'];
-
- protected $thumbnailType = null;
-
- /**
- * @var Medium[]
- */
- protected $alternatives = [];
-
- /**
- * @var array
- */
- protected $attributes = [];
-
- /**
- * @var array
- */
- protected $styleAttributes = [];
-
- /**
- * @var array
- */
- protected $metadata = [];
-
- /**
- * @var array
- */
- protected $medium_querystring = [];
-
- protected $timestamp;
-
/**
* Construct.
*
* @param array $items
- * @param Blueprint $blueprint
+ * @param Blueprint|null $blueprint
*/
public function __construct($items = [], Blueprint $blueprint = null)
{
@@ -85,99 +45,21 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface
}
$this->def('mime', 'application/octet-stream');
- $this->reset();
- }
-
- public function __clone()
- {
- // Allows future compatibility as parent::__clone() works.
- }
-
- /**
- * Create a copy of this media object
- *
- * @return Medium
- */
- public function copy()
- {
- return clone $this;
- }
-
- /**
- * Return just metadata from the Medium object
- *
- * @return Data
- */
- public function meta()
- {
- return new Data($this->items);
- }
-
- /**
- * Check if this medium exists or not
- *
- * @return bool
- */
- public function exists()
- {
- $path = $this->get('filepath');
- if (file_exists($path)) {
- return true;
- }
- return false;
- }
- /**
- * Get file modification time for the medium.
- *
- * @return int|null
- */
- public function modified()
- {
- $path = $this->get('filepath');
-
- if (!file_exists($path)) {
- return null;
+ if (!$this->offsetExists('size')) {
+ $path = $this->get('filepath');
+ $this->def('size', filesize($path));
}
- return filemtime($path) ?: null;
- }
-
- /**
- * @return int
- */
- public function size()
- {
- $path = $this->get('filepath');
-
- if (!file_exists($path)) {
- return 0;
- }
-
- return filesize($path) ?: 0;
- }
-
- /**
- * Set querystring to file modification timestamp (or value provided as a parameter).
- *
- * @param string|int|null $timestamp
- * @return $this
- */
- public function setTimestamp($timestamp = null)
- {
- $this->timestamp = (string)($timestamp ?? $this->modified());
-
- return $this;
+ $this->reset();
}
/**
- * Returns an array containing just the metadata
- *
- * @return array
+ * Clone medium.
*/
- public function metadata()
+ public function __clone()
{
- return $this->metadata;
+ // Allows future compatibility as parent::__clone() works.
}
/**
@@ -192,21 +74,15 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface
}
/**
- * Add alternative Medium to this Medium.
- *
- * @param int|float $ratio
- * @param Medium $alternative
+ * @return array
*/
- public function addAlternative($ratio, Medium $alternative)
+ public function getMeta(): array
{
- if (!is_numeric($ratio) || $ratio === 0) {
- return;
- }
-
- $alternative->set('ratio', $ratio);
- $width = $alternative->get('width');
-
- $this->alternatives[$width] = $alternative;
+ return [
+ 'mime' => $this->mime,
+ 'size' => $this->size,
+ 'modified' => $this->modified,
+ ];
}
/**
@@ -220,460 +96,35 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface
}
/**
- * Return PATH to file.
- *
- * @param bool $reset
- * @return string path to file
- */
- public function path($reset = true)
- {
- if ($reset) {
- $this->reset();
- }
-
- return $this->get('filepath');
- }
-
- /**
- * Return the relative path to file
- *
- * @param bool $reset
- * @return mixed
- */
- public function relativePath($reset = true)
- {
- $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
-
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($output)) {
- $output = $locator->findResource($output, false);
- }
-
- if ($reset) {
- $this->reset();
- }
-
- return str_replace(GRAV_ROOT, '', $output);
- }
-
- /**
- * Return URL to file.
- *
- * @param bool $reset
- * @return string
- */
- public function url($reset = true)
- {
- $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
-
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($output)) {
- $output = $locator->findResource($output, false);
- }
-
- if ($reset) {
- $this->reset();
- }
-
- return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
- }
-
- /**
- * Get/set querystring for the file's url
- *
- * @param string $querystring
- * @param bool $withQuestionmark
- * @return string
- */
- public function querystring($querystring = null, $withQuestionmark = true)
- {
- if (null !== $querystring) {
- $this->medium_querystring[] = ltrim($querystring, '?&');
- foreach ($this->alternatives as $alt) {
- $alt->querystring($querystring, $withQuestionmark);
- }
- }
-
- if (empty($this->medium_querystring)) {
- return '';
- }
-
- // join the strings
- $querystring = implode('&', $this->medium_querystring);
- // explode all strings
- $query_parts = explode('&', $querystring);
- // Join them again now ensure the elements are unique
- $querystring = implode('&', array_unique($query_parts));
-
- return $withQuestionmark ? ('?' . $querystring) : $querystring;
- }
-
- /**
- * Get the URL with full querystring
- *
- * @param string $url
- * @return string
- */
- public function urlQuerystring($url)
- {
- $querystring = $this->querystring();
- if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) {
- $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp);
- }
-
- return ltrim($url . $querystring . $this->urlHash(), '/');
- }
-
- /**
- * Get/set hash for the file's url
- *
- * @param string $hash
- * @param bool $withHash
- * @return string
- */
- public function urlHash($hash = null, $withHash = true)
- {
- if ($hash) {
- $this->set('urlHash', ltrim($hash, '#'));
- }
-
- $hash = $this->get('urlHash', '');
-
- return $withHash && !empty($hash) ? '#' . $hash : $hash;
- }
-
- /**
- * Get an element (is array) that can be rendered by the Parsedown engine
- *
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
- * @param bool $reset
- * @return array
- */
- public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
- {
- $attributes = $this->attributes;
-
- $style = '';
- foreach ($this->styleAttributes as $key => $value) {
- if (is_numeric($key)) // Special case for inline style attributes, refer to style() method
- $style .= $value;
- else
- $style .= $key . ': ' . $value . ';';
- }
- if ($style) {
- $attributes['style'] = $style;
- }
-
- if (empty($attributes['title'])) {
- if (!empty($title)) {
- $attributes['title'] = $title;
- } elseif (!empty($this->items['title'])) {
- $attributes['title'] = $this->items['title'];
- }
- }
-
- if (empty($attributes['alt'])) {
- if (!empty($alt)) {
- $attributes['alt'] = $alt;
- } elseif (!empty($this->items['alt'])) {
- $attributes['alt'] = $this->items['alt'];
- } elseif (!empty($this->items['alt_text'])) {
- $attributes['alt'] = $this->items['alt_text'];
- } else {
- $attributes['alt'] = '';
- }
- }
-
- if (empty($attributes['class'])) {
- if (!empty($class)) {
- $attributes['class'] = $class;
- } elseif (!empty($this->items['class'])) {
- $attributes['class'] = $this->items['class'];
- }
- }
-
- if (empty($attributes['id'])) {
- if (!empty($id)) {
- $attributes['id'] = $id;
- } elseif (!empty($this->items['id'])) {
- $attributes['id'] = $this->items['id'];
- }
- }
-
- switch ($this->mode) {
- case 'text':
- $element = $this->textParsedownElement($attributes, false);
- break;
- case 'thumbnail':
- $element = $this->getThumbnail()->sourceParsedownElement($attributes, false);
- break;
- case 'source':
- $element = $this->sourceParsedownElement($attributes, false);
- break;
- default:
- $element = [];
- }
-
- if ($reset) {
- $this->reset();
- }
-
- $this->display('source');
-
- return $element;
- }
-
- /**
- * Parsedown element for source display mode
- *
- * @param array $attributes
- * @param bool $reset
- * @return array
- */
- protected function sourceParsedownElement(array $attributes, $reset = true)
- {
- return $this->textParsedownElement($attributes, $reset);
- }
-
- /**
- * Parsedown element for text display mode
- *
- * @param array $attributes
- * @param bool $reset
- * @return array
- */
- protected function textParsedownElement(array $attributes, $reset = true)
- {
- $text = empty($attributes['title']) ? empty($attributes['alt']) ? $this->get('filename') : $attributes['alt'] : $attributes['title'];
-
- $element = [
- 'name' => 'p',
- 'attributes' => $attributes,
- 'text' => $text
- ];
-
- if ($reset) {
- $this->reset();
- }
-
- return $element;
- }
-
- /**
- * Reset medium.
- *
- * @return $this
- */
- public function reset()
- {
- $this->attributes = [];
- return $this;
- }
-
- /**
- * Switch display mode.
- *
- * @param string $mode
- *
- * @return $this
- */
- public function display($mode = 'source')
- {
- if ($this->mode === $mode) {
- return $this;
- }
-
-
- $this->mode = $mode;
-
- return $mode === 'thumbnail' ? ($this->getThumbnail() ? $this->getThumbnail()->reset() : null) : $this->reset();
- }
-
- /**
- * Helper method to determine if this media item has a thumbnail or not
- *
- * @param string $type;
- *
- * @return bool
+ * @param string $thumb
*/
- public function thumbnailExists($type = 'page')
+ protected function createThumbnail($thumb)
{
- $thumbs = $this->get('thumbnails');
- if (isset($thumbs[$type])) {
- return true;
- }
- return false;
+ return MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
}
/**
- * Switch thumbnail.
- *
- * @param string $type
- *
- * @return $this
- */
- public function thumbnail($type = 'auto')
- {
- if ($type !== 'auto' && !\in_array($type, $this->thumbnailTypes, true)) {
- return $this;
- }
-
- if ($this->thumbnailType !== $type) {
- $this->_thumbnail = null;
- }
-
- $this->thumbnailType = $type;
-
- return $this;
- }
-
-
- /**
- * Turn the current Medium into a Link
- *
- * @param bool $reset
- * @param array $attributes
- * @return Link
+ * @param array $attributes
+ * @return MediaLinkInterface
*/
- public function link($reset = true, array $attributes = [])
+ protected function createLink(array $attributes)
{
- if ($this->mode !== 'source') {
- $this->display('source');
- }
-
- foreach ($this->attributes as $key => $value) {
- empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value;
- }
-
- empty($attributes['href']) && $attributes['href'] = $this->url();
-
return new Link($attributes, $this);
}
/**
- * Turn the current Medium into a Link with lightbox enabled
- *
- * @param int $width
- * @param int $height
- * @param bool $reset
- * @return Link
- */
- public function lightbox($width = null, $height = null, $reset = true)
- {
- $attributes = ['rel' => 'lightbox'];
-
- if ($width && $height) {
- $attributes['data-width'] = $width;
- $attributes['data-height'] = $height;
- }
-
- return $this->link($reset, $attributes);
- }
-
- /**
- * Add a class to the element from Markdown or Twig
- * Example:  or 
- *
- * @return $this
- */
- public function classes()
- {
- $classes = func_get_args();
- if (!empty($classes)) {
- $this->attributes['class'] = implode(',', $classes);
- }
-
- return $this;
- }
-
- /**
- * Add an id to the element from Markdown or Twig
- * Example: 
- *
- * @param string $id
- * @return $this
- */
- public function id($id)
- {
- if (is_string($id)) {
- $this->attributes['id'] = trim($id);
- }
-
- return $this;
- }
-
- /**
- * Allows to add an inline style attribute from Markdown or Twig
- * Example: 
- *
- * @param string $style
- * @return $this
- */
- public function style($style)
- {
- $this->styleAttributes[] = rtrim($style, ';') . ';';
- return $this;
- }
-
- /**
- * Allow any action to be called on this medium from twig or markdown
- *
- * @param string $method
- * @param mixed $args
- * @return $this
+ * @return Grav
*/
- public function __call($method, $args)
+ protected function getGrav(): Grav
{
- $qs = $method;
- if (\count($args) > 1 || (\count($args) === 1 && !empty($args[0]))) {
- $qs .= '=' . implode(',', array_map(function ($a) {
- if (is_array($a)) {
- $a = '[' . implode(',', $a) . ']';
- }
- return rawurlencode($a);
- }, $args));
- }
-
- if (!empty($qs)) {
- $this->querystring($this->querystring(null, false) . '&' . $qs);
- }
-
- return $this;
+ return Grav::instance();
}
/**
- * Get the thumbnail Medium object
- *
- * @return ThumbnailImageMedium
+ * @return array
*/
- protected function getThumbnail()
+ protected function getItems(): array
{
- if (!$this->_thumbnail) {
- $types = $this->thumbnailTypes;
-
- if ($this->thumbnailType !== 'auto') {
- array_unshift($types, $this->thumbnailType);
- }
-
- foreach ($types as $type) {
- $thumb = $this->get('thumbnails.' . $type, false);
-
- if ($thumb) {
- $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
- $thumb->parent = $this;
- }
-
- if ($thumb) {
- $this->_thumbnail = $thumb;
- break;
- }
- }
- }
-
- return $this->_thumbnail;
+ return $this->items;
}
-
}
diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php
index dc372d81..7dee4ea1 100644
--- a/system/src/Grav/Common/Page/Medium/MediumFactory.php
+++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,8 +11,17 @@ namespace Grav\Common\Page\Medium;
use Grav\Common\Grav;
use Grav\Common\Data\Blueprint;
+use Grav\Common\Media\Interfaces\ImageMediaInterface;
+use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Framework\Form\FormFlashFile;
+use Psr\Http\Message\UploadedFileInterface;
+use function dirname;
+use function is_array;
+/**
+ * Class MediumFactory
+ * @package Grav\Common\Page\Medium
+ */
class MediumFactory
{
/**
@@ -20,7 +29,7 @@ class MediumFactory
*
* @param string $file
* @param array $params
- * @return Medium
+ * @return Medium|null
*/
public static function fromFile($file, array $params = [])
{
@@ -31,16 +40,21 @@ class MediumFactory
$parts = pathinfo($file);
$path = $parts['dirname'];
$filename = $parts['basename'];
- $ext = $parts['extension'];
+ $ext = $parts['extension'] ?? '';
$basename = $parts['filename'];
$config = Grav::instance()['config'];
- $media_params = $config->get('media.types.' . strtolower($ext));
- if (!\is_array($media_params)) {
+ $media_params = $ext ? $config->get('media.types.' . strtolower($ext)) : null;
+ if (!is_array($media_params)) {
return null;
}
+ // Remove empty 'image' attribute
+ if (isset($media_params['image']) && empty($media_params['image'])) {
+ unset($media_params['image']);
+ }
+
$params += $media_params;
// Add default settings for undefined variables.
@@ -71,23 +85,33 @@ class MediumFactory
/**
* Create Medium from an uploaded file
*
- * @param FormFlashFile $uploadedFile
+ * @param UploadedFileInterface $uploadedFile
* @param array $params
- * @return Medium
+ * @return Medium|null
*/
- public static function fromUploadedFile(FormFlashFile $uploadedFile, array $params = [])
+ public static function fromUploadedFile(UploadedFileInterface $uploadedFile, array $params = [])
{
- $parts = pathinfo($uploadedFile->getClientFilename());
+ // For now support only FormFlashFiles, which exist over multiple requests. Also ignore errored and moved media.
+ if (!$uploadedFile instanceof FormFlashFile || $uploadedFile->getError() !== \UPLOAD_ERR_OK || $uploadedFile->isMoved()) {
+ return null;
+ }
+
+ $clientName = $uploadedFile->getClientFilename();
+ if (!$clientName) {
+ return null;
+ }
+
+ $parts = pathinfo($clientName);
$filename = $parts['basename'];
- $ext = $parts['extension'];
+ $ext = $parts['extension'] ?? '';
$basename = $parts['filename'];
$file = $uploadedFile->getTmpFile();
- $path = dirname($file);
+ $path = $file ? dirname($file) : '';
$config = Grav::instance()['config'];
- $media_params = $config->get('media.types.' . strtolower($ext));
- if (!\is_array($media_params)) {
+ $media_params = $ext ? $config->get('media.types.' . strtolower($ext)) : null;
+ if (!is_array($media_params)) {
return null;
}
@@ -104,7 +128,7 @@ class MediumFactory
'basename' => $basename,
'extension' => $ext,
'path' => $path,
- 'modified' => filemtime($file),
+ 'modified' => $file ? filemtime($file) : 0,
'thumbnails' => []
];
@@ -149,14 +173,14 @@ class MediumFactory
/**
* Create a new ImageMedium by scaling another ImageMedium object.
*
- * @param ImageMedium $medium
+ * @param ImageMediaInterface|MediaObjectInterface $medium
* @param int $from
* @param int $to
- * @return Medium|array
+ * @return ImageMediaInterface|MediaObjectInterface|array
*/
public static function scaledFromMedium($medium, $from, $to)
{
- if (! $medium instanceof ImageMedium) {
+ if (!$medium instanceof ImageMedium) {
return $medium;
}
diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
index c6d75bc7..44ab9529 100644
--- a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
+++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,20 +12,22 @@ namespace Grav\Common\Page\Medium;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Page\Markdown\Excerpts;
+/**
+ * Trait ParsedownHtmlTrait
+ * @package Grav\Common\Page\Medium
+ */
trait ParsedownHtmlTrait
{
- /**
- * @var \Grav\Common\Markdown\Parsedown
- */
+ /** @var Parsedown|null */
protected $parsedown;
/**
* Return HTML markup from the medium.
*
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
* @param bool $reset
* @return string
*/
diff --git a/system/src/Grav/Common/Page/Medium/RenderableInterface.php b/system/src/Grav/Common/Page/Medium/RenderableInterface.php
index 50a6e679..ac724470 100644
--- a/system/src/Grav/Common/Page/Medium/RenderableInterface.php
+++ b/system/src/Grav/Common/Page/Medium/RenderableInterface.php
@@ -3,34 +3,39 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+/**
+ * Interface RenderableInterface
+ * @package Grav\Common\Page\Medium
+ */
interface RenderableInterface
{
/**
* Return HTML markup from the medium.
*
- * @param string $title
- * @param string $alt
- * @param string $class
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
* @param bool $reset
* @return string
*/
- public function html($title = null, $alt = null, $class = null, $reset = true);
+ public function html($title = null, $alt = null, $class = null, $id = null, $reset = true);
/**
* Return Parsedown Element from the medium.
*
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
+ * @param string|null $title
+ * @param string|null $alt
+ * @param string|null $class
+ * @param string|null $id
* @param bool $reset
- * @return string
+ * @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true);
}
diff --git a/system/src/Grav/Common/Page/Medium/StaticImageMedium.php b/system/src/Grav/Common/Page/Medium/StaticImageMedium.php
index 1fa54397..ba92fa1c 100644
--- a/system/src/Grav/Common/Page/Medium/StaticImageMedium.php
+++ b/system/src/Grav/Common/Page/Medium/StaticImageMedium.php
@@ -3,15 +3,24 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
-class StaticImageMedium extends Medium
+use Grav\Common\Media\Interfaces\ImageMediaInterface;
+use Grav\Common\Media\Traits\ImageLoadingTrait;
+use Grav\Common\Media\Traits\StaticResizeTrait;
+
+/**
+ * Class StaticImageMedium
+ * @package Grav\Common\Page\Medium
+ */
+class StaticImageMedium extends Medium implements ImageMediaInterface
{
use StaticResizeTrait;
+ use ImageLoadingTrait;
/**
* Parsedown element for source display mode
@@ -22,7 +31,9 @@ class StaticImageMedium extends Medium
*/
protected function sourceParsedownElement(array $attributes, $reset = true)
{
- empty($attributes['src']) && $attributes['src'] = $this->url($reset);
+ if (empty($attributes['src'])) {
+ $attributes['src'] = $this->url($reset);
+ }
return ['name' => 'img', 'attributes' => $attributes];
}
diff --git a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php
index 8d6d43fb..d95b2d63 100644
--- a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php
+++ b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php
@@ -3,26 +3,22 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+use Grav\Common\Media\Traits\StaticResizeTrait as NewResizeTrait;
+
+user_error('Grav\Common\Page\Medium\StaticResizeTrait is deprecated since Grav 1.7, use Grav\Common\Media\Traits\StaticResizeTrait instead', E_USER_DEPRECATED);
+
+/**
+ * Trait StaticResizeTrait
+ * @package Grav\Common\Page\Medium
+ * @deprecated 1.7 Use `Grav\Common\Media\Traits\StaticResizeTrait` instead
+ */
trait StaticResizeTrait
{
- /**
- * Resize media by setting attributes
- *
- * @param int $width
- * @param int $height
- * @return $this
- */
- public function resize($width = null, $height = null)
- {
- $this->styleAttributes['width'] = $width . 'px';
- $this->styleAttributes['height'] = $height . 'px';
-
- return $this;
- }
+ use NewResizeTrait;
}
diff --git a/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php
index 380baee6..a56a20d1 100644
--- a/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php
+++ b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php
@@ -3,130 +3,19 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
+use Grav\Common\Media\Traits\ThumbnailMediaTrait;
+
+/**
+ * Class ThumbnailImageMedium
+ * @package Grav\Common\Page\Medium
+ */
class ThumbnailImageMedium extends ImageMedium
{
- /**
- * @var Medium
- */
- public $parent = null;
-
- /**
- * @var bool
- */
- public $linked = false;
-
- /**
- * Return srcset string for this Medium and its alternatives.
- *
- * @param bool $reset
- * @return string
- */
- public function srcset($reset = true)
- {
- return '';
- }
-
- /**
- * Get an element (is array) that can be rendered by the Parsedown engine
- *
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
- * @param bool $reset
- * @return array
- */
- public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
- {
- return $this->bubble('parsedownElement', [$title, $alt, $class, $id, $reset]);
- }
-
- /**
- * Return HTML markup from the medium.
- *
- * @param string $title
- * @param string $alt
- * @param string $class
- * @param string $id
- * @param bool $reset
- * @return string
- */
- public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
- {
- return $this->bubble('html', [$title, $alt, $class, $id, $reset]);
- }
-
- /**
- * Switch display mode.
- *
- * @param string $mode
- *
- * @return $this
- */
- public function display($mode = 'source')
- {
- return $this->bubble('display', [$mode], false);
- }
-
- /**
- * Switch thumbnail.
- *
- * @param string $type
- *
- * @return $this
- */
- public function thumbnail($type = 'auto')
- {
- $this->bubble('thumbnail', [$type], false);
-
- return $this->bubble('getThumbnail', [], false);
- }
-
- /**
- * Turn the current Medium into a Link
- *
- * @param bool $reset
- * @param array $attributes
- * @return Link
- */
- public function link($reset = true, array $attributes = [])
- {
- return $this->bubble('link', [$reset, $attributes], false);
- }
-
- /**
- * Turn the current Medium into a Link with lightbox enabled
- *
- * @param int $width
- * @param int $height
- * @param bool $reset
- * @return Link
- */
- public function lightbox($width = null, $height = null, $reset = true)
- {
- return $this->bubble('lightbox', [$width, $height, $reset], false);
- }
-
- /**
- * Bubble a function call up to either the superclass function or the parent Medium instance
- *
- * @param string $method
- * @param array $arguments
- * @param bool $testLinked
- * @return Medium
- */
- protected function bubble($method, array $arguments = [], $testLinked = true)
- {
- if (!$testLinked || $this->linked) {
- return $this->parent ? call_user_func_array(array($this->parent, $method), $arguments) : $this;
- }
-
- return call_user_func_array(array($this, 'parent::' . $method), $arguments);
- }
+ use ThumbnailMediaTrait;
}
diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php
index c0b488aa..bfcf550c 100644
--- a/system/src/Grav/Common/Page/Medium/VideoMedium.php
+++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php
@@ -3,148 +3,22 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
-class VideoMedium extends Medium
-{
- use StaticResizeTrait;
-
- /**
- * Parsedown element for source display mode
- *
- * @param array $attributes
- * @param bool $reset
- * @return array
- */
- protected function sourceParsedownElement(array $attributes, $reset = true)
- {
- $location = $this->url($reset);
-
- return [
- 'name' => 'video',
- 'text' => 'Your browser does not support the video tag.',
- 'attributes' => $attributes
- ];
- }
+use Grav\Common\Media\Interfaces\VideoMediaInterface;
+use Grav\Common\Media\Traits\VideoMediaTrait;
- /**
- * Allows to set or remove the HTML5 default controls
- *
- * @param bool $display
- * @return $this
- */
- public function controls($display = true)
- {
- if($display) {
- $this->attributes['controls'] = true;
- } else {
- unset($this->attributes['controls']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the video's poster image
- *
- * @param string $urlImage
- * @return $this
- */
- public function poster($urlImage)
- {
- $this->attributes['poster'] = $urlImage;
-
- return $this;
- }
-
- /**
- * Allows to set the loop attribute
- *
- * @param bool $status
- * @return $this
- */
- public function loop($status = false)
- {
- if($status) {
- $this->attributes['loop'] = true;
- } else {
- unset($this->attributes['loop']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the autoplay attribute
- *
- * @param bool $status
- * @return $this
- */
- public function autoplay($status = false)
- {
- if ($status) {
- $this->attributes['autoplay'] = '';
- } else {
- unset($this->attributes['autoplay']);
- }
-
- return $this;
- }
-
- /**
- * Allows ability to set the preload option
- *
- * @param null $status
- * @return $this
- */
- public function preload($status = null)
- {
- if ($status) {
- $this->attributes['preload'] = $status;
- } else {
- unset($this->attributes['preload']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the playsinline attribute
- *
- * @param bool $status
- * @return $this
- */
- public function playsinline($status = false)
- {
- if($status) {
- $this->attributes['playsinline'] = true;
- } else {
- unset($this->attributes['playsinline']);
- }
-
- return $this;
- }
-
- /**
- * Allows to set the muted attribute
- *
- * @param bool $status
- * @return $this
- */
- public function muted($status = false)
- {
- if($status) {
- $this->attributes['muted'] = true;
- } else {
- unset($this->attributes['muted']);
- }
-
- return $this;
- }
+/**
+ * Class VideoMedium
+ * @package Grav\Common\Page\Medium
+ */
+class VideoMedium extends Medium implements VideoMediaInterface
+{
+ use VideoMediaTrait;
/**
* Reset medium.
@@ -155,7 +29,7 @@ class VideoMedium extends Medium
{
parent::reset();
- $this->attributes['controls'] = true;
+ $this->resetPlayer();
return $this;
}
diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php
index 176a2788..2099be21 100644
--- a/system/src/Grav/Common/Page/Page.php
+++ b/system/src/Grav/Common/Page/Page.php
@@ -3,107 +3,163 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use Exception;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
+use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
+use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Markdown\Excerpts;
-use Grav\Common\Taxonomy;
+use Grav\Common\Page\Traits\PageFormTrait;
+use Grav\Common\Twig\Twig;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Yaml;
-use Negotiation\Accept;
-use Negotiation\Negotiator;
+use Grav\Framework\Flex\Flex;
+use InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
+use RuntimeException;
+use SplFileInfo;
+use function dirname;
+use function in_array;
+use function is_array;
+use function is_object;
+use function is_string;
+use function strlen;
define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
+/**
+ * Class Page
+ * @package Grav\Common\Page
+ */
class Page implements PageInterface
{
+ use PageFormTrait;
use MediaTrait;
- /**
- * @var string Filename. Leave as null if page is folder.
- */
+ /** @var string|null Filename. Leave as null if page is folder. */
protected $name;
+ /** @var bool */
+ protected $initialized = false;
+ /** @var string */
protected $folder;
+ /** @var string */
protected $path;
+ /** @var string */
protected $extension;
+ /** @var string */
protected $url_extension;
-
+ /** @var string */
protected $id;
+ /** @var string */
protected $parent;
+ /** @var string */
protected $template;
+ /** @var int */
protected $expires;
+ /** @var string */
protected $cache_control;
+ /** @var bool */
protected $visible;
+ /** @var bool */
protected $published;
+ /** @var int */
protected $publish_date;
+ /** @var int|null */
protected $unpublish_date;
+ /** @var string */
protected $slug;
+ /** @var string */
protected $route;
+ /** @var string|null */
protected $raw_route;
+ /** @var string */
protected $url;
+ /** @var array */
protected $routes;
+ /** @var bool */
protected $routable;
+ /** @var int */
protected $modified;
+ /** @var string */
protected $redirect;
+ /** @var string */
protected $external_url;
- protected $items;
+ /** @var object|null */
protected $header;
+ /** @var string */
protected $frontmatter;
+ /** @var string */
protected $language;
+ /** @var string|null */
protected $content;
+ /** @var array */
protected $content_meta;
+ /** @var string|null */
protected $summary;
+ /** @var string */
protected $raw_content;
- protected $pagination;
+ /** @var array|null */
protected $metadata;
+ /** @var string */
protected $title;
+ /** @var int */
protected $max_count;
+ /** @var string */
protected $menu;
+ /** @var int */
protected $date;
+ /** @var string */
protected $dateformat;
+ /** @var array */
protected $taxonomy;
+ /** @var string */
protected $order_by;
+ /** @var string */
protected $order_dir;
+ /** @var array|string|null */
protected $order_manual;
- protected $modular;
+ /** @var bool */
protected $modular_twig;
+ /** @var array */
protected $process;
+ /** @var int|null */
protected $summary_size;
+ /** @var bool */
protected $markdown_extra;
+ /** @var bool */
protected $etag;
+ /** @var bool */
protected $last_modified;
+ /** @var string */
protected $home_route;
+ /** @var bool */
protected $hide_home_route;
+ /** @var bool */
protected $ssl;
+ /** @var string */
protected $template_format;
+ /** @var bool */
protected $debugger;
- /** @var array */
- protected $forms;
- /**
- * @var PageInterface Unmodified (original) version of the page. Used for copying and moving the page.
- */
+ /** @var PageInterface|null Unmodified (original) version of the page. Used for copying and moving the page. */
private $_original;
-
- /**
- * @var string Action
- */
+ /** @var string Action */
private $_action;
/**
@@ -122,15 +178,16 @@ class Page implements PageInterface
/**
* Initializes the page instance variables based on a file
*
- * @param \SplFileInfo $file The file information for the .md file that the page represents
- * @param string $extension
- *
+ * @param SplFileInfo $file The file information for the .md file that the page represents
+ * @param string|null $extension
* @return $this
*/
- public function init(\SplFileInfo $file, $extension = null)
+ public function init(SplFileInfo $file, $extension = null)
{
$config = Grav::instance()['config'];
+ $this->initialized = true;
+
// some extension logic
if (empty($extension)) {
$this->extension('.' . $file->getExtension());
@@ -158,10 +215,12 @@ class Page implements PageInterface
$this->published();
$this->urlExtension();
-
return $this;
}
+ /**
+ * @return void
+ */
protected function processFrontmatter()
{
// Quick check for twig output tags in frontmatter if enabled
@@ -183,22 +242,38 @@ class Page implements PageInterface
* Return an array with the routes of other translated languages
*
* @param bool $onlyPublished only return published translations
- *
* @return array the page translated languages
*/
public function translatedLanguages($onlyPublished = false)
{
- $filename = substr($this->name, 0, -(strlen($this->extension())));
- $config = Grav::instance()['config'];
- $languages = $config->get('system.languages.supported', []);
+ $grav = Grav::instance();
+
+ /** @var Language $language */
+ $language = $grav['language'];
+
+ $languages = $language->getLanguages();
+ $defaultCode = $language->getDefault();
+
+ $name = substr($this->name, 0, -strlen($this->extension()));
$translatedLanguages = [];
- foreach ($languages as $language) {
- $path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md';
- if (file_exists($path)) {
- $aPage = new Page();
- $aPage->init(new \SplFileInfo($path), $language . '.md');
+ foreach ($languages as $languageCode) {
+ $languageExtension = ".{$languageCode}.md";
+ $path = $this->path . DS . $this->folder . DS . $name . $languageExtension;
+ $exists = file_exists($path);
+
+ // Default language may be saved without language file location.
+ if (!$exists && $languageCode === $defaultCode) {
+ $languageExtension = '.md';
+ $path = $this->path . DS . $this->folder . DS . $name . $languageExtension;
+ $exists = file_exists($path);
+ }
+ if ($exists) {
+ $aPage = new Page();
+ $aPage->init(new SplFileInfo($path), $languageExtension);
+ $aPage->route($this->route());
+ $aPage->rawRoute($this->rawRoute());
$route = $aPage->header()->routes['default'] ?? $aPage->rawRoute();
if (!$route) {
$route = $aPage->route();
@@ -208,7 +283,7 @@ class Page implements PageInterface
continue;
}
- $translatedLanguages[$language] = $route;
+ $translatedLanguages[$languageCode] = $route;
}
}
@@ -219,37 +294,25 @@ class Page implements PageInterface
* Return an array listing untranslated languages available
*
* @param bool $includeUnpublished also list unpublished translations
- *
* @return array the page untranslated languages
*/
public function untranslatedLanguages($includeUnpublished = false)
{
- $filename = substr($this->name, 0, -strlen($this->extension()));
- $config = Grav::instance()['config'];
- $languages = $config->get('system.languages.supported', []);
- $untranslatedLanguages = [];
+ $grav = Grav::instance();
- foreach ($languages as $language) {
- $path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md';
- if (file_exists($path)) {
- $aPage = new Page();
- $aPage->init(new \SplFileInfo($path), $language . '.md');
- if ($includeUnpublished && !$aPage->published()) {
- $untranslatedLanguages[] = $language;
- }
- } else {
- $untranslatedLanguages[] = $language;
- }
- }
+ /** @var Language $language */
+ $language = $grav['language'];
+
+ $languages = $language->getLanguages();
+ $translated = array_keys($this->translatedLanguages(!$includeUnpublished));
- return $untranslatedLanguages;
+ return array_values(array_diff($languages, $translated));
}
/**
* Gets and Sets the raw data
*
- * @param string $var Raw content string
- *
+ * @param string|null $var Raw content string
* @return string Raw content string
*/
public function raw($var = null)
@@ -282,7 +345,6 @@ class Page implements PageInterface
*/
public function frontmatter($var = null)
{
-
if ($var) {
$this->frontmatter = (string)$var;
@@ -305,8 +367,7 @@ class Page implements PageInterface
/**
* Gets and Sets the header based on the YAML configuration at the top of the .md file
*
- * @param object|array $var a YAML object representing the configuration for the file
- *
+ * @param object|array|null $var a YAML object representing the configuration for the file
* @return object the current YAML configuration
*/
public function header($var = null)
@@ -337,8 +398,10 @@ class Page implements PageInterface
$frontmatterFile = CompiledYamlFile::instance($this->path . '/' . $this->folder . '/frontmatter.yaml');
if ($frontmatterFile->exists()) {
$frontmatter_data = (array)$frontmatterFile->content();
- $this->header = (object)array_replace_recursive($frontmatter_data,
- (array)$this->header);
+ $this->header = (object)array_replace_recursive(
+ $frontmatter_data,
+ (array)$this->header
+ );
$frontmatterFile->free();
}
// Process frontmatter with Twig if enabled
@@ -346,7 +409,7 @@ class Page implements PageInterface
$this->processFrontmatter();
}
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$file->raw(Grav::instance()['language']->translate([
'GRAV.FRONTMATTER_ERROR_PAGE',
$this->slug(),
@@ -360,8 +423,6 @@ class Page implements PageInterface
}
$var = true;
}
-
-
}
if ($var) {
@@ -465,8 +526,7 @@ class Page implements PageInterface
/**
* Get page language
*
- * @param string $var
- *
+ * @param string|null $var
* @return mixed
*/
public function language($var = null)
@@ -497,6 +557,9 @@ class Page implements PageInterface
return (int)($this->header()->http_response_code ?? 200);
}
+ /**
+ * @return array
+ */
public function httpHeaders()
{
$headers = [];
@@ -545,10 +608,8 @@ class Page implements PageInterface
/**
* Get the summary.
*
- * @param int $size Max summary size.
- *
+ * @param int|null $size Max summary size.
* @param bool $textOnly Only count text size.
- *
* @return string
*/
public function summary($size = null, $textOnly = false)
@@ -568,8 +629,7 @@ class Page implements PageInterface
$content = $textOnly ? strip_tags($this->content()) : $this->content();
$summary_size = $this->summary_size;
} else {
- $content = strip_tags($this->summary);
- // Use mb_strwidth to deal with the 2 character widths characters
+ $content = $textOnly ? strip_tags($this->summary) : $this->summary;
$summary_size = mb_strwidth($content, 'utf-8');
}
@@ -580,7 +640,7 @@ class Page implements PageInterface
return $content;
}
if (($format === 'short') && isset($summary_size)) {
- // Use mb_strimwidth to slice the string
+ // Slice the string
if (mb_strwidth($content, 'utf8') > $summary_size) {
return mb_substr($content, 0, $summary_size);
}
@@ -608,7 +668,7 @@ 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);
@@ -629,8 +689,7 @@ class Page implements PageInterface
/**
* Gets and Sets the content based on content portion of the .md file
*
- * @param string $var Content
- *
+ * @param string|null $var Content
* @return string Content
*/
public function content($var = null)
@@ -673,14 +732,20 @@ class Page implements PageInterface
$process_markdown = $this->shouldProcess('markdown');
$process_twig = $this->shouldProcess('twig') || $this->modularTwig();
- $cache_enable = $this->header->cache_enable ?? $config->get('system.cache.enabled',
- true);
- $twig_first = $this->header->twig_first ?? $config->get('system.pages.twig_first',
- true);
+ $cache_enable = $this->header->cache_enable ?? $config->get(
+ 'system.cache.enabled',
+ true
+ );
+ $twig_first = $this->header->twig_first ?? $config->get(
+ 'system.pages.twig_first',
+ false
+ );
// never cache twig means it's always run after content
- $never_cache_twig = $this->header->never_cache_twig ?? $config->get('system.pages.never_cache_twig',
- false);
+ $never_cache_twig = $this->header->never_cache_twig ?? $config->get(
+ 'system.pages.never_cache_twig',
+ true
+ );
// if no cached-content run everything
if ($never_cache_twig) {
@@ -703,7 +768,6 @@ class Page implements PageInterface
if ($process_twig) {
$this->processTwig();
}
-
} else {
if ($this->content === false || $cache_enable === false) {
$this->content = $this->raw_content;
@@ -719,10 +783,9 @@ class Page implements PageInterface
// Content Processed but not cached yet
Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
-
} else {
if ($process_markdown) {
- $this->processMarkdown();
+ $this->processMarkdown($process_twig);
}
// Content Processed but not cached yet
@@ -772,7 +835,7 @@ class Page implements PageInterface
* Add an entry to the page's contentMeta array
*
* @param string $name
- * @param string $value
+ * @param mixed $value
*/
public function addContentMeta($name, $value)
{
@@ -784,16 +847,12 @@ class Page implements PageInterface
*
* @param string|null $name
*
- * @return string
+ * @return mixed|null
*/
public function getContentMeta($name = null)
{
if ($name) {
- if (isset($this->content_meta[$name])) {
- return $this->content_meta[$name];
- }
-
- return null;
+ return $this->content_meta[$name] ?? null;
}
return $this->content_meta;
@@ -813,8 +872,11 @@ class Page implements PageInterface
/**
* Process the Markdown content. Uses Parsedown or Parsedown Extra depending on configuration
+ *
+ * @param bool $keepTwig If true, content between twig tags will not be processed.
+ * @return void
*/
- protected function processMarkdown()
+ protected function processMarkdown(bool $keepTwig = false)
{
/** @var Config $config */
$config = Grav::instance()['config'];
@@ -846,24 +908,55 @@ class Page implements PageInterface
$parsedown = new Parsedown($excerpts);
}
- $this->content = $parsedown->text($this->content);
+ $content = $this->content;
+ if ($keepTwig) {
+ $token = [
+ '/' . Utils::generateRandomString(3),
+ Utils::generateRandomString(3) . '/'
+ ];
+ // Base64 encode any twig.
+ $content = preg_replace_callback(
+ ['/({#.*?#})/mu', '/({{.*?}})/mu', '/({%.*?%})/mu'],
+ static function ($matches) use ($token) { return $token[0] . base64_encode($matches[1]) . $token[1]; },
+ $content
+ );
+ }
+
+ $content = $parsedown->text($content);
+
+ if ($keepTwig) {
+ // Base64 decode the encoded twig.
+ $content = preg_replace_callback(
+ ['`' . $token[0] . '([A-Za-z0-9+/]+={0,2})' . $token[1] . '`mu'],
+ static function ($matches) { return base64_decode($matches[1]); },
+ $content
+ );
+ }
+
+ $this->content = $content;
}
/**
* Process the Twig page content.
+ *
+ * @return void
*/
private function processTwig()
{
+ /** @var Twig $twig */
$twig = Grav::instance()['twig'];
$this->content = $twig->processPage($this, $this->content);
}
/**
* Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page
+ *
+ * @return void
*/
public function cachePageContent()
{
+ /** @var Cache $cache */
$cache = Grav::instance()['cache'];
$cache_id = md5('page' . $this->getCacheKey());
$cache->save($cache_id, ['content' => $this->content, 'content_meta' => $this->content_meta]);
@@ -883,6 +976,7 @@ class Page implements PageInterface
* Needed by the onPageContentProcessed event to set the raw page content
*
* @param string $content
+ * @return void
*/
public function setRawContent($content)
{
@@ -894,7 +988,6 @@ class Page implements PageInterface
*
* @param string $name Variable name.
* @param mixed $default
- *
* @return mixed
*/
public function value($name, $default = null)
@@ -922,13 +1015,16 @@ class Page implements PageInterface
return $this->slug();
}
if ($name === 'name') {
+ $name = $this->name();
$language = $this->language() ? '.' . $this->language() : '';
- $name_val = str_replace($language . '.md', '', $this->name());
- if ($this->modular()) {
- return 'modular/' . $name_val;
+ $pattern = '%(' . preg_quote($language, '%') . ')?\.md$%';
+ $name = preg_replace($pattern, '', $name);
+
+ if ($this->isModule()) {
+ return 'modular/' . $name;
}
- return $name_val;
+ return $name;
}
if ($name === 'media') {
return $this->media()->all();
@@ -975,7 +1071,6 @@ class Page implements PageInterface
* Gets and Sets the Page raw content
*
* @param string|null $var
- *
* @return string
*/
public function rawMarkdown($var = null)
@@ -987,6 +1082,15 @@ class Page implements PageInterface
return $this->raw_content;
}
+ /**
+ * @return bool
+ * @internal
+ */
+ public function translated(): bool
+ {
+ return $this->initialized;
+ }
+
/**
* Get file object to the page.
*
@@ -1004,7 +1108,7 @@ class Page implements PageInterface
/**
* Save page if there's a file assigned to it.
*
- * @param bool|mixed $reorder Internal use.
+ * @param bool|array $reorder Internal use.
*/
public function save($reorder = true)
{
@@ -1024,6 +1128,14 @@ class Page implements PageInterface
$this->doReorder($reorder);
}
+ // We need to signal Flex Pages about the change.
+ /** @var Flex|null $flex */
+ $flex = Grav::instance()['flex'] ?? null;
+ $directory = $flex ? $flex->getDirectory('pages') : null;
+ if (null !== $directory) {
+ $directory->clearCache();
+ }
+
$this->_original = null;
}
@@ -1033,7 +1145,6 @@ class Page implements PageInterface
* You need to call $this->save() in order to perform the move.
*
* @param PageInterface $parent New parent page.
- *
* @return $this
*/
public function move(PageInterface $parent)
@@ -1046,10 +1157,10 @@ class Page implements PageInterface
$this->_action = 'move';
if ($this->route() === $parent->route()) {
- throw new \RuntimeException('Failed: Cannot set page parent to self');
+ throw new RuntimeException('Failed: Cannot set page parent to self');
}
if (Utils::startsWith($parent->rawRoute(), $this->rawRoute())) {
- throw new \RuntimeException('Failed: Cannot set page parent to a child of current page');
+ throw new RuntimeException('Failed: Cannot set page parent to a child of current page');
}
$this->parent($parent);
@@ -1077,7 +1188,6 @@ class Page implements PageInterface
* You need to call $this->save() in order to perform the move.
*
* @param PageInterface $parent New parent page.
- *
* @return $this
*/
public function copy(PageInterface $parent)
@@ -1132,7 +1242,8 @@ class Page implements PageInterface
/**
* Validate page header.
*
- * @throws \Exception
+ * @return void
+ * @throws Exception
*/
public function validate()
{
@@ -1142,6 +1253,8 @@ class Page implements PageInterface
/**
* Filter page header from illegal contents.
+ *
+ * @return void
*/
public function filter()
{
@@ -1205,92 +1318,10 @@ class Page implements PageInterface
return $this->id();
}
- /**
- * Returns normalized list of name => form pairs.
- *
- * @return array
- */
- public function forms()
- {
- if (null === $this->forms) {
- $header = $this->header();
-
- // Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
- $grav = Grav::instance();
- $grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header]));
-
- $rules = $header->rules ?? null;
- if (!\is_array($rules)) {
- $rules = [];
- }
-
- $forms = [];
-
- // First grab page.header.form
- $form = $this->normalizeForm($header->form ?? null, null, $rules);
- if ($form) {
- $forms[$form['name']] = $form;
- }
-
- // Append page.header.forms (override singular form if it clashes)
- $headerForms = $header->forms ?? null;
- if (\is_array($headerForms)) {
- foreach ($headerForms as $name => $form) {
- $form = $this->normalizeForm($form, $name, $rules);
- if ($form) {
- $forms[$form['name']] = $form;
- }
- }
- }
-
- $this->forms = $forms;
- }
-
- return $this->forms;
- }
-
- /**
- * @param array $new
- */
- public function addForms(array $new)
- {
- // Initialize forms.
- $this->forms();
-
- foreach ($new as $form) {
- $form = $this->normalizeForm($form);
- if ($form) {
- $this->forms[$form['name']] = $form;
- }
- }
- }
-
- protected function normalizeForm($form, $name = null, array $rules = [])
- {
- if (!\is_array($form)) {
- return null;
- }
-
- // Ignore numeric indexes on name.
- if (!$name || (string)(int)$name === (string)$name) {
- $name = null;
- }
-
- $name = $name ?? $form['name'] ?? $this->slug();
-
- $formRules = $form['rules'] ?? null;
- if (!\is_array($formRules)) {
- $formRules = [];
- }
-
- return ['name' => $name, 'rules' => $rules + $formRules] + $form;
- }
-
/**
* Gets and sets the associated media as found in the page folder.
*
- * @param Media $var Representation of associated media.
- *
+ * @param Media|null $var Representation of associated media.
* @return Media Representation of associated media.
*/
public function media($var = null)
@@ -1299,7 +1330,10 @@ class Page implements PageInterface
$this->setMedia($var);
}
- return $this->getMedia();
+ /** @var Media $media */
+ $media = $this->getMedia();
+
+ return $media;
}
/**
@@ -1327,8 +1361,7 @@ class Page implements PageInterface
/**
* Gets and sets the name field. If no name field is set, it will return 'default.md'.
*
- * @param string $var The name of this page.
- *
+ * @param string|null $var The name of this page.
* @return string The name of this page.
*/
public function name($var = null)
@@ -1354,8 +1387,7 @@ class Page implements PageInterface
* Gets and sets the template field. This is used to find the correct Twig template file to render.
* If no field is set, it will return the name without the .md extension
*
- * @param string $var the template name
- *
+ * @param string|null $var the template name
* @return string the template name
*/
public function template($var = null)
@@ -1364,74 +1396,37 @@ class Page implements PageInterface
$this->template = $var;
}
if (empty($this->template)) {
- $this->template = ($this->modular() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name());
+ $this->template = ($this->isModule() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name());
}
return $this->template;
}
/**
- * Allows a page to override the output render format, usually the extension provided
- * in the URL. (e.g. `html`, `json`, `xml`, etc).
- *
- * @param null $var
+ * Allows a page to override the output render format, usually the extension provided in the URL.
+ * (e.g. `html`, `json`, `xml`, etc).
*
- * @return null
+ * @param string|null $var
+ * @return string
*/
public function templateFormat($var = null)
{
- if ($var !== null) {
- $this->template_format = $var;
- return $this->template_format;
- }
-
- if (isset($this->template_format)) {
- return $this->template_format;
- }
-
- // Set from URL extension set on page
- $page_extension = trim($this->header->append_url_extension ?? '' , '.');
- if (!empty($page_extension)) {
- $this->template_format = $page_extension;
-
- return $this->template_format;
+ if (null !== $var) {
+ $this->template_format = is_string($var) ? $var : null;
}
- // Set from uri extension
- $uri_extension = Grav::instance()['uri']->extension();
- if (is_string($uri_extension)) {
- $this->template_format = $uri_extension;
-
- return $this->template_format;
- }
-
- // Use content negotiation via the `accept:` header
- $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
- if (is_string($http_accept)) {
- $negotiator = new Negotiator();
-
- $supported_types = Utils::getSupportPageTypes(['html', 'json']);
- $priorities = Utils::getMimeTypes($supported_types);
-
- $media_type = $negotiator->getBest($http_accept, $priorities);
- $mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
-
- $this->template_format = Utils::getExtensionByMime($mimetype);
-
- return $this->template_format;
+ if (!isset($this->template_format)) {
+ $this->template_format = ltrim($this->header->append_url_extension ?? Utils::getPageFormat(), '.');
}
- // Last chance set a default type
- $this->template_format = 'html';
return $this->template_format;
}
/**
* Gets and sets the extension field.
*
- * @param null $var
- *
- * @return null|string
+ * @param string|null $var
+ * @return string
*/
public function extension($var = null)
{
@@ -1468,8 +1463,7 @@ class Page implements PageInterface
/**
* Gets and sets the expires field. If not set will return the default
*
- * @param int $var The new expires value.
- *
+ * @param int|null $var The new expires value.
* @return int The expires value
*/
public function expires($var = null)
@@ -1485,8 +1479,8 @@ class Page implements PageInterface
* Gets and sets the cache-control property. If not set it will return the default value (null)
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options
*
- * @param null $var
- * @return null
+ * @param string|null $var
+ * @return string|null
*/
public function cacheControl($var = null)
{
@@ -1500,8 +1494,7 @@ class Page implements PageInterface
/**
* Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name
*
- * @param string $var the title of the Page
- *
+ * @param string|null $var the title of the Page
* @return string the title of the Page
*/
public function title($var = null)
@@ -1520,8 +1513,7 @@ class Page implements PageInterface
* Gets and sets the menu name for this Page. This is the text that can be used specifically for navigation.
* If no menu field is set, it will use the title()
*
- * @param string $var the menu field for the page
- *
+ * @param string|null $var the menu field for the page
* @return string the menu field for the page
*/
public function menu($var = null)
@@ -1539,8 +1531,7 @@ class Page implements PageInterface
/**
* Gets and Sets whether or not this Page is visible for navigation
*
- * @param bool $var true if the page is visible
- *
+ * @param bool|null $var true if the page is visible
* @return bool true if the page is visible
*/
public function visible($var = null)
@@ -1565,8 +1556,7 @@ class Page implements PageInterface
/**
* Gets and Sets whether or not this Page is considered published
*
- * @param bool $var true if the page is published
- *
+ * @param bool|null $var true if the page is published
* @return bool true if the page is published
*/
public function published($var = null)
@@ -1586,8 +1576,7 @@ class Page implements PageInterface
/**
* Gets and Sets the Page publish date
*
- * @param string $var string representation of a date
- *
+ * @param string|null $var string representation of a date
* @return int unix timestamp representation of the date
*/
public function publishDate($var = null)
@@ -1602,8 +1591,7 @@ class Page implements PageInterface
/**
* Gets and Sets the Page unpublish date
*
- * @param string $var string representation of a date
- *
+ * @param string|null $var string representation of a date
* @return int|null unix timestamp representation of the date
*/
public function unpublishDate($var = null)
@@ -1620,8 +1608,7 @@ class Page implements PageInterface
* via a URL.
* The page must be *routable* and *published*
*
- * @param bool $var true if the page is routable
- *
+ * @param bool|null $var true if the page is routable
* @return bool true if the page is routable
*/
public function routable($var = null)
@@ -1633,6 +1620,10 @@ class Page implements PageInterface
return $this->routable && $this->published();
}
+ /**
+ * @param bool|null $var
+ * @return bool
+ */
public function ssl($var = null)
{
if ($var !== null) {
@@ -1646,8 +1637,7 @@ class Page implements PageInterface
* Gets and Sets the process setup for this Page. This is multi-dimensional array that consists of
* a simple array of arrays with the form array("markdown"=>true) for example
*
- * @param array $var an Array of name value pairs where the name is the process and value is true or false
- *
+ * @param array|null $var an Array of name value pairs where the name is the process and value is true or false
* @return array an Array of name value pairs where the name is the process and value is true or false
*/
public function process($var = null)
@@ -1660,9 +1650,9 @@ class Page implements PageInterface
}
/**
- * Returns the state of the debugger override etting for this page
+ * Returns the state of the debugger override setting for this page
*
- * @return mixed
+ * @return bool
*/
public function debugger()
{
@@ -1673,8 +1663,7 @@ class Page implements PageInterface
* Function to merge page metadata tags and build an array of Metadata objects
* that can then be rendered in the page.
*
- * @param array $var an Array of metadata values to set
- *
+ * @param array|null $var an Array of metadata values to set
* @return array an Array of metadata values for the page
*/
public function metadata($var = null)
@@ -1685,16 +1674,21 @@ class Page implements PageInterface
// if not metadata yet, process it.
if (null === $this->metadata) {
- $header_tag_http_equivs = ['content-type', 'default-style', 'refresh', 'x-ua-compatible'];
+ $header_tag_http_equivs = ['content-type', 'default-style', 'refresh', 'x-ua-compatible', 'content-security-policy'];
$this->metadata = [];
- $metadata = [];
// Set the Generator tag
- $metadata['generator'] = 'GravCMS';
+ $metadata = [
+ 'generator' => 'GravCMS'
+ ];
+
+ $config = Grav::instance()['config'];
+
+ $escape = !$config->get('system.strict_mode.twig_compat', false) || $config->get('system.twig.autoescape', true);
// Get initial metadata for the page
- $metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata', []));
+ $metadata = array_merge($metadata, $config->get('site.metadata', []));
if (isset($this->header->metadata) && is_array($this->header->metadata)) {
// Merge any site.metadata settings in with page metadata
@@ -1713,28 +1707,28 @@ class Page implements PageInterface
$this->metadata[$prop_key] = [
'name' => $prop_key,
'property' => $prop_key,
- 'content' => htmlspecialchars($prop_value, ENT_QUOTES, 'UTF-8')
+ 'content' => $escape ? htmlspecialchars($prop_value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $prop_value
];
}
} else {
// If it this is a standard meta data type
if ($value) {
- if (\in_array($key, $header_tag_http_equivs, true)) {
+ if (in_array($key, $header_tag_http_equivs, true)) {
$this->metadata[$key] = [
'http_equiv' => $key,
- 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')
+ 'content' => $escape ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value
];
} elseif ($key === 'charset') {
- $this->metadata[$key] = ['charset' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')];
+ $this->metadata[$key] = ['charset' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value];
} else {
// if it's a social metadata with separator, render as property
$separator = strpos($key, ':');
$hasSeparator = $separator && $separator < strlen($key) - 1;
$entry = [
- 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')
+ 'content' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value
];
- if ($hasSeparator && !Utils::startsWith($key, 'twitter')) {
+ if ($hasSeparator && !Utils::startsWith($key, ['twitter', 'flattr'])) {
$entry['property'] = $key;
} else {
$entry['name'] = $key;
@@ -1762,8 +1756,7 @@ class Page implements PageInterface
* Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses
* the parent folder from the path
*
- * @param string $var the slug, e.g. 'my-blog'
- *
+ * @param string|null $var the slug, e.g. 'my-blog'
* @return string the slug
*/
public function slug($var = null)
@@ -1782,9 +1775,8 @@ class Page implements PageInterface
/**
* Get/set order number of this page.
*
- * @param int $var
- *
- * @return int|bool
+ * @param int|null $var
+ * @return string|bool
*/
public function order($var = null)
{
@@ -1804,7 +1796,6 @@ class Page implements PageInterface
* Gets the URL for a page - alias of url().
*
* @param bool $include_host
- *
* @return string the permalink
*/
public function link($include_host = false)
@@ -1825,7 +1816,6 @@ class Page implements PageInterface
* Returns the canonical URL for a page
*
* @param bool $include_lang
- *
* @return string
*/
public function canonical($include_lang = true)
@@ -1840,7 +1830,6 @@ class Page implements PageInterface
* @param bool $canonical True to return the canonical URL
* @param bool $include_base Include base url on multisite as well as language code
* @param bool $raw_route
- *
* @return string The url.
*/
public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false)
@@ -1878,11 +1867,6 @@ class Page implements PageInterface
$uri = $grav['uri'];
$url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
- // trim trailing / if not root
- if ($url !== '/') {
- $url = rtrim($url, '/');
- }
-
return Uri::filterPath($url);
}
@@ -1890,9 +1874,8 @@ class Page implements PageInterface
* Gets the route for the page based on the route headers if available, else from
* the parents route and the current Page's slug.
*
- * @param string $var Set new default route.
- *
- * @return string The route for the Page.
+ * @param string|null $var Set new default route.
+ * @return string|null The route for the Page.
*/
public function route($var = null)
{
@@ -1937,8 +1920,7 @@ class Page implements PageInterface
/**
* Gets and Sets the page raw route
*
- * @param null $var
- *
+ * @param string|null $var
* @return null|string
*/
public function rawRoute($var = null)
@@ -1962,8 +1944,7 @@ class Page implements PageInterface
/**
* Gets the route aliases for the page based on page headers.
*
- * @param array $var list of route aliases
- *
+ * @param array|null $var list of route aliases
* @return array The route aliases for the Page.
*/
public function routeAliases($var = null)
@@ -1983,8 +1964,7 @@ class Page implements PageInterface
* Gets the canonical route for this page if its set. If provided it will use
* that value, else if it's `true` it will use the default route.
*
- * @param null $var
- *
+ * @param string|null $var
* @return bool|string
*/
public function routeCanonical($var = null)
@@ -2003,8 +1983,7 @@ class Page implements PageInterface
/**
* Gets and sets the identifier for this Page object.
*
- * @param string $var the identifier
- *
+ * @param string|null $var the identifier
* @return string the identifier
*/
public function id($var = null)
@@ -2026,8 +2005,7 @@ class Page implements PageInterface
/**
* Gets and sets the modified timestamp.
*
- * @param int $var modified unix timestamp
- *
+ * @param int|null $var modified unix timestamp
* @return int modified unix timestamp
*/
public function modified($var = null)
@@ -2042,9 +2020,8 @@ class Page implements PageInterface
/**
* Gets the redirect set in the header.
*
- * @param string $var redirect url
- *
- * @return string
+ * @param string|null $var redirect url
+ * @return string|null
*/
public function redirect($var = null)
{
@@ -2052,17 +2029,16 @@ class Page implements PageInterface
$this->redirect = $var;
}
- return $this->redirect;
+ return $this->redirect ?: null;
}
/**
* Gets and sets the option to show the etag header for the page.
*
- * @param bool $var show etag header
- *
+ * @param bool|null $var show etag header
* @return bool show etag header
*/
- public function eTag($var = null)
+ public function eTag($var = null): bool
{
if ($var !== null) {
$this->etag = $var;
@@ -2071,14 +2047,13 @@ class Page implements PageInterface
$this->etag = (bool)Grav::instance()['config']->get('system.pages.etag');
}
- return $this->etag;
+ return $this->etag ?? false;
}
/**
* Gets and sets the option to show the last_modified header for the page.
*
- * @param bool $var show last_modified header
- *
+ * @param bool|null $var show last_modified header
* @return bool show last_modified header
*/
public function lastModified($var = null)
@@ -2096,8 +2071,7 @@ class Page implements PageInterface
/**
* Gets and sets the path to the .md file for this Page object.
*
- * @param string $var the file path
- *
+ * @param string|null $var the file path
* @return string|null the file path
*/
public function filePath($var = null)
@@ -2111,7 +2085,7 @@ class Page implements PageInterface
$this->path = dirname($var, 2);
}
- return $this->path . '/' . $this->folder . '/' . ($this->name ?: '');
+ return rtrim($this->path . '/' . $this->folder . '/' . ($this->name() ?: ''), '/');
}
/**
@@ -2121,11 +2095,13 @@ class Page implements PageInterface
*/
public function filePathClean()
{
- return str_replace(ROOT_DIR, '', $this->filePath());
+ return str_replace(GRAV_ROOT . DS, '', $this->filePath());
}
/**
* Returns the clean path to the page file
+ *
+ * @return string
*/
public function relativePagePath()
{
@@ -2136,8 +2112,7 @@ class Page implements PageInterface
* Gets and sets the path to the folder where the .md for this Page object resides.
* This is equivalent to the filePath but without the filename.
*
- * @param string $var the path
- *
+ * @param string|null $var the path
* @return string|null the path
*/
public function path($var = null)
@@ -2155,8 +2130,7 @@ class Page implements PageInterface
/**
* Get/set the folder.
*
- * @param string $var Optional path
- *
+ * @param string|null $var Optional path
* @return string|null
*/
public function folder($var = null)
@@ -2171,8 +2145,7 @@ class Page implements PageInterface
/**
* Gets and sets the date for this Page object. This is typically passed in via the page headers
*
- * @param string $var string representation of a date
- *
+ * @param string|null $var string representation of a date
* @return int unix timestamp representation of the date
*/
public function date($var = null)
@@ -2192,8 +2165,7 @@ class Page implements PageInterface
* Gets and sets the date format for this Page object. This is typically passed in via the page headers
* using typical PHP date string structure - http://php.net/manual/en/function.date.php
*
- * @param string $var string representation of a date format
- *
+ * @param string|null $var string representation of a date format
* @return string string representation of a date format
*/
public function dateformat($var = null)
@@ -2208,8 +2180,7 @@ class Page implements PageInterface
/**
* Gets and sets the order by which any sub-pages should be sorted.
*
- * @param string $var the order, either "asc" or "desc"
- *
+ * @param string|null $var the order, either "asc" or "desc"
* @return string the order, either "asc" or "desc"
* @deprecated 1.6
*/
@@ -2236,8 +2207,7 @@ class Page implements PageInterface
* date - is the order based on the date set in the pages
* folder - is the order based on the name of the folder with any numerics omitted
*
- * @param string $var supported options include "default", "title", "date", and "folder"
- *
+ * @param string|null $var supported options include "default", "title", "date", and "folder"
* @return string supported options include "default", "title", "date", and "folder"
* @deprecated 1.6
*/
@@ -2255,8 +2225,7 @@ class Page implements PageInterface
/**
* Gets the manual order set in the header.
*
- * @param string $var supported options include "default", "title", "date", and "folder"
- *
+ * @param string|null $var supported options include "default", "title", "date", and "folder"
* @return array
* @deprecated 1.6
*/
@@ -2275,8 +2244,7 @@ class Page implements PageInterface
* Gets and sets the maxCount field which describes how many sub-pages should be displayed if the
* sub_pages header property is set for this page object.
*
- * @param int $var the maximum number of sub-pages
- *
+ * @param int|null $var the maximum number of sub-pages
* @return int the maximum number of sub-pages
* @deprecated 1.6
*/
@@ -2299,19 +2267,18 @@ class Page implements PageInterface
/**
* Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with.
*
- * @param array $var an array of taxonomies
- *
+ * @param array|null $var an array of taxonomies
* @return array an array of taxonomies
*/
public function taxonomy($var = null)
{
if ($var !== null) {
// make sure first level are arrays
- array_walk($var, function(&$value) {
+ array_walk($var, static function (&$value) {
$value = (array) $value;
});
// make sure all values are strings
- array_walk_recursive($var, function(&$value) {
+ array_walk_recursive($var, static function (&$value) {
$value = (string) $value;
});
$this->taxonomy = $var;
@@ -2323,12 +2290,14 @@ class Page implements PageInterface
/**
* Gets and sets the modular var that helps identify this page is a modular child
*
- * @param bool $var true if modular_twig
- *
+ * @param bool|null $var true if modular_twig
* @return bool true if modular_twig
+ * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead.
*/
public function modular($var = null)
{
+ user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->isModule() or ->modularTwig() method instead', E_USER_DEPRECATED);
+
return $this->modularTwig($var);
}
@@ -2336,8 +2305,7 @@ class Page implements PageInterface
* Gets and sets the modular_twig var that helps identify this page as a modular child page that will need
* twig processing handled differently from a regular page.
*
- * @param bool $var true if modular_twig
- *
+ * @param bool|null $var true if modular_twig
* @return bool true if modular_twig
*/
public function modularTwig($var = null)
@@ -2353,14 +2321,13 @@ class Page implements PageInterface
}
}
- return $this->modular_twig;
+ return $this->modular_twig ?? false;
}
/**
* Gets the configured state of the processing method.
*
* @param string $process the process, eg "twig" or "markdown"
- *
* @return bool whether or not the processing method is enabled for this Page
*/
public function shouldProcess($process)
@@ -2371,8 +2338,7 @@ class Page implements PageInterface
/**
* Gets and Sets the parent object for this page
*
- * @param PageInterface $var the parent page object
- *
+ * @param PageInterface|null $var the parent page object
* @return PageInterface|null the parent page object if it exists.
*/
public function parent(PageInterface $var = null)
@@ -2390,17 +2356,13 @@ class Page implements PageInterface
}
/**
- * Gets the top parent object for this page
+ * Gets the top parent object for this page. Can return page itself.
*
- * @return PageInterface|null the top parent page object if it exists.
+ * @return PageInterface The top parent page object.
*/
public function topParent()
{
- $topParent = $this->parent();
-
- if (!$topParent) {
- return null;
- }
+ $topParent = $this;
while (true) {
$theParent = $topParent->parent();
@@ -2417,7 +2379,7 @@ class Page implements PageInterface
/**
* Returns children of this page.
*
- * @return Collection
+ * @return PageCollectionInterface|Collection
*/
public function children()
{
@@ -2484,8 +2446,7 @@ class Page implements PageInterface
* Returns the adjacent sibling based on a direction.
*
* @param int $direction either -1 or +1
- *
- * @return PageInterface|bool the sibling page
+ * @return PageInterface|false the sibling page
*/
public function adjacentSibling($direction = 1)
{
@@ -2501,7 +2462,7 @@ class Page implements PageInterface
/**
* Returns the item in the current position.
*
- * @return int the index of the current page.
+ * @return int|null The index of the current page.
*/
public function currentPosition()
{
@@ -2535,21 +2496,23 @@ class Page implements PageInterface
*/
public function activeChild()
{
- $uri = Grav::instance()['uri'];
- $pages = Grav::instance()['pages'];
+ $grav = Grav::instance();
+ /** @var Uri $uri */
+ $uri = $grav['uri'];
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
$uri_path = rtrim(urldecode($uri->path()), '/');
- $routes = Grav::instance()['pages']->routes();
+ $routes = $pages->routes();
if (isset($routes[$uri_path])) {
- /** @var PageInterface $child_page */
- $child_page = $pages->dispatch($uri->route())->parent();
- if ($child_page) {
- while (!$child_page->root()) {
- if ($this->path() === $child_page->path()) {
- return true;
- }
- $child_page = $child_page->parent();
+ $page = $pages->find($uri->route());
+ /** @var PageInterface|null $child_page */
+ $child_page = $page ? $page->parent() : null;
+ while ($child_page && !$child_page->root()) {
+ if ($this->path() === $child_page->path()) {
+ return true;
}
+ $child_page = $child_page->parent();
}
}
@@ -2581,8 +2544,7 @@ class Page implements PageInterface
/**
* Helper method to return an ancestor page.
*
- * @param bool $lookup Name of the parent folder
- *
+ * @param bool|null $lookup Name of the parent folder
* @return PageInterface page you were looking for if it exists
*/
public function ancestor($lookup = null)
@@ -2598,12 +2560,11 @@ class Page implements PageInterface
* page object is returned.
*
* @param string $field Name of the parent folder
- *
* @return PageInterface
*/
public function inherited($field)
{
- list($inherited, $currentParams) = $this->getInheritedParams($field);
+ [$inherited, $currentParams] = $this->getInheritedParams($field);
$this->modifyHeader($field, $currentParams);
@@ -2620,7 +2581,7 @@ class Page implements PageInterface
*/
public function inheritedField($field)
{
- list($inherited, $currentParams) = $this->getInheritedParams($field);
+ [$inherited, $currentParams] = $this->getInheritedParams($field);
return $currentParams;
}
@@ -2629,7 +2590,6 @@ class Page implements PageInterface
* Method that contains shared logic for inherited() and inheritedField()
*
* @param string $field Name of the parent folder
- *
* @return array
*/
protected function getInheritedParams($field)
@@ -2669,327 +2629,52 @@ class Page implements PageInterface
* @param string|array $params
* @param bool $pagination
*
- * @return Collection
- * @throws \InvalidArgumentException
+ * @return PageCollectionInterface|Collection
+ * @throws InvalidArgumentException
*/
public function collection($params = 'content', $pagination = true)
{
if (is_string($params)) {
+ // Look into a page header field.
$params = (array)$this->value('header.' . $params);
} elseif (!is_array($params)) {
- throw new \InvalidArgumentException('Argument should be either header variable name or array of parameters');
- }
-
- if (!isset($params['items'])) {
- return new Collection();
- }
-
- // See if require published filter is set and use that, if assume published=true
- $only_published = true;
- if (isset($params['filter']['published']) && $params['filter']['published']) {
- $only_published = false;
- } elseif (isset($params['filter']['non-published']) && $params['filter']['non-published']) {
- $only_published = false;
- }
-
- $collection = $this->evaluate($params['items'], $only_published);
- if (!$collection instanceof Collection) {
- $collection = new Collection();
- }
- $collection->setParams($params);
-
- /** @var Uri $uri */
- $uri = Grav::instance()['uri'];
- /** @var Config $config */
- $config = Grav::instance()['config'];
-
- $process_taxonomy = $params['url_taxonomy_filters'] ?? $config->get('system.pages.url_taxonomy_filters');
-
- if ($process_taxonomy) {
- foreach ((array)$config->get('site.taxonomies') as $taxonomy) {
- if ($uri->param(rawurlencode($taxonomy))) {
- $items = explode(',', $uri->param($taxonomy));
- $collection->setParams(['taxonomies' => [$taxonomy => $items]]);
-
- foreach ($collection as $page) {
- // Don't filter modular pages
- if ($page->modular()) {
- continue;
- }
- foreach ($items as $item) {
- $item = rawurldecode($item);
- if (empty($page->taxonomy[$taxonomy]) || !\in_array(htmlspecialchars_decode($item, ENT_QUOTES), $page->taxonomy[$taxonomy], true)
- ) {
- $collection->remove($page->path());
- }
- }
- }
- }
- }
+ throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
}
- // If a filter or filters are set, filter the collection...
- if (isset($params['filter'])) {
-
- // remove any inclusive sets from filer:
- $sets = ['published', 'visible', 'modular', 'routable'];
- foreach ($sets as $type) {
- $var = "non-{$type}";
- if (isset($params['filter'][$type], $params['filter'][$var]) && $params['filter'][$type] && $params['filter'][$var]) {
- unset ($params['filter'][$type], $params['filter'][$var]);
- }
- }
-
- foreach ((array)$params['filter'] as $type => $filter) {
- switch ($type) {
- case 'published':
- if ((bool) $filter) {
- $collection->published();
- }
- break;
- case 'non-published':
- if ((bool) $filter) {
- $collection->nonPublished();
- }
- break;
- case 'visible':
- if ((bool) $filter) {
- $collection->visible();
- }
- break;
- case 'non-visible':
- if ((bool) $filter) {
- $collection->nonVisible();
- }
- break;
- case 'modular':
- if ((bool) $filter) {
- $collection->modular();
- }
- break;
- case 'non-modular':
- if ((bool) $filter) {
- $collection->nonModular();
- }
- break;
- case 'routable':
- if ((bool) $filter) {
- $collection->routable();
- }
- break;
- case 'non-routable':
- if ((bool) $filter) {
- $collection->nonRoutable();
- }
- break;
- case 'type':
- $collection->ofType($filter);
- break;
- case 'types':
- $collection->ofOneOfTheseTypes($filter);
- break;
- case 'access':
- $collection->ofOneOfTheseAccessLevels($filter);
- break;
- }
- }
- }
-
- if (isset($params['dateRange'])) {
- $start = $params['dateRange']['start'] ?? 0;
- $end = $params['dateRange']['end'] ?? false;
- $field = $params['dateRange']['field'] ?? false;
- $collection->dateRange($start, $end, $field);
- }
-
- if (isset($params['order'])) {
- $by = $params['order']['by'] ?? 'default';
- $dir = $params['order']['dir'] ?? 'asc';
- $custom = $params['order']['custom'] ?? null;
- $sort_flags = $params['order']['sort_flags'] ?? null;
-
- if (is_array($sort_flags)) {
- $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
- $sort_flags = array_reduce($sort_flags, function ($a, $b) {
- return $a | $b;
- }, 0); //merge constant values using bit or
- }
-
- $collection->order($by, $dir, $custom, $sort_flags);
- }
-
- /** @var Grav $grav */
- $grav = Grav::instance();
-
- // New Custom event to handle things like pagination.
- $grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection]));
-
- // Slice and dice the collection if pagination is required
- if ($pagination) {
- $params = $collection->params();
-
- $limit = (int)($params['limit'] ?? 0);
- $start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0;
+ $params['filter'] = ($params['filter'] ?? []) + ['translated' => true];
+ $context = [
+ 'pagination' => $pagination,
+ 'self' => $this
+ ];
- if ($limit && $collection->count() > $limit) {
- $collection->slice($start, $limit);
- }
- }
+ /** @var Pages $pages */
+ $pages = Grav::instance()['pages'];
- return $collection;
+ return $pages->getCollection($params, $context);
}
/**
* @param string|array $value
* @param bool $only_published
- * @return mixed
+ * @return PageCollectionInterface|Collection
*/
public function evaluate($value, $only_published = true)
{
- // Parse command.
- if (is_string($value)) {
- // Format: @command.param
- $cmd = $value;
- $params = [];
- } elseif (is_array($value) && count($value) == 1 && !is_int(key($value))) {
- // Format: @command.param: { attr1: value1, attr2: value2 }
- $cmd = (string)key($value);
- $params = (array)current($value);
- } else {
- $result = [];
- foreach ((array)$value as $key => $val) {
- if (is_int($key)) {
- $result = $result + $this->evaluate($val, $only_published)->toArray();
- } else {
- $result = $result + $this->evaluate([$key => $val], $only_published)->toArray();
- }
-
- }
-
- return new Collection($result);
- }
+ $params = [
+ 'items' => $value,
+ 'published' => $only_published
+ ];
+ $context = [
+ 'event' => false,
+ 'pagination' => false,
+ 'url_taxonomy_filters' => false,
+ 'self' => $this
+ ];
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
- $parts = explode('.', $cmd);
- $current = array_shift($parts);
-
- /** @var Collection $results */
- $results = new Collection();
-
- switch ($current) {
- case 'self@':
- case '@self':
- if (!empty($parts)) {
- switch ($parts[0]) {
- case 'modular':
- // @self.modular: false (alternative to @self.children)
- if (!empty($params) && $params[0] === false) {
- $results = $this->children()->nonModular();
- break;
- }
- $results = $this->children()->modular();
- break;
- case 'children':
- $results = $this->children()->nonModular();
- break;
- case 'all':
- $results = $this->children();
- break;
- case 'parent':
- $collection = new Collection();
- $results = $collection->addPage($this->parent());
- break;
- case 'siblings':
- if (!$this->parent()) {
- return new Collection();
- }
- $results = $this->parent()->children()->remove($this->path());
- break;
- case 'descendants':
- $results = $pages->all($this)->remove($this->path())->nonModular();
- break;
- }
- }
-
-
- break;
-
- case 'page@':
- case '@page':
- $page = null;
-
- if (!empty($params)) {
- $page = $this->find($params[0]);
- }
-
- // safety check in case page is not found
- if (!isset($page)) {
- return $results;
- }
-
- // Handle a @page.descendants
- if (!empty($parts)) {
- switch ($parts[0]) {
- case 'modular':
- $results = new Collection();
- foreach ($page->children() as $child) {
- $results = $results->addPage($child);
- }
- $results->modular();
- break;
- case 'page':
- case 'self':
- $results = new Collection();
- $results = $results->addPage($page);
- break;
-
- case 'descendants':
- $results = $pages->all($page)->remove($page->path())->nonModular();
- break;
-
- case 'children':
- $results = $page->children()->nonModular();
- break;
- }
- } else {
- $results = $page->children()->nonModular();
- }
-
- break;
-
- case 'root@':
- case '@root':
- if (!empty($parts) && $parts[0] === 'descendants') {
- $results = $pages->all($pages->root())->nonModular();
- } else {
- $results = $pages->root()->children()->nonModular();
- }
- break;
-
- case 'taxonomy@':
- case '@taxonomy':
- // Gets a collection of pages by using one of the following formats:
- // @taxonomy.category: blog
- // @taxonomy.category: [ blog, featured ]
- // @taxonomy: { category: [ blog, featured ], level: 1 }
-
- /** @var Taxonomy $taxonomy_map */
- $taxonomy_map = Grav::instance()['taxonomy'];
-
- if (!empty($parts)) {
- $params = [implode('.', $parts) => $params];
- }
- $results = $taxonomy_map->findTaxonomy($params);
- break;
- }
-
- if ($only_published) {
- $results = $results->published();
- }
-
- return $results;
+ return $pages->getCollection($params, $context);
}
/**
@@ -3016,6 +2701,14 @@ class Page implements PageInterface
return !$this->isPage();
}
+ /**
+ * @return bool
+ */
+ public function isModule(): bool
+ {
+ return $this->modularTwig();
+ }
+
/**
* Returns whether the page exists in the filesystem.
*
@@ -3042,7 +2735,6 @@ class Page implements PageInterface
* Cleans the path.
*
* @param string $path the path
- *
* @return string the path
*/
protected function cleanPath($path)
@@ -3107,8 +2799,8 @@ class Page implements PageInterface
* Moves or copies the page in filesystem.
*
* @internal
- *
- * @throws \Exception
+ * @return void
+ * @throws Exception
*/
protected function doRelocation()
{
@@ -3130,9 +2822,11 @@ class Page implements PageInterface
rename($path . '/' . $this->_original->name(), $path . '/' . $this->name());
}
}
-
}
+ /**
+ * @return void
+ */
protected function setPublishState()
{
// Handle publishing dates if no explicit published option set
@@ -3154,6 +2848,10 @@ class Page implements PageInterface
}
}
+ /**
+ * @param string $route
+ * @return string
+ */
protected function adjustRouteCase($route)
{
$case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls');
@@ -3168,16 +2866,16 @@ class Page implements PageInterface
*/
public function getOriginal()
{
- return $this->_original;
+ return $this->_original;
}
/**
* Gets the action.
*
- * @return string The Action string.
+ * @return string|null The Action string.
*/
public function getAction()
{
- return $this->_action;
+ return $this->_action;
}
}
diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php
index 8f31afb0..941c9a7b 100644
--- a/system/src/Grav/Common/Page/Pages.php
+++ b/system/src/Grav/Common/Page/Pages.php
@@ -3,132 +3,152 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use Exception;
+use FilesystemIterator;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Blueprints;
+use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder;
+use Grav\Common\Flex\Types\Pages\PageCollection;
+use Grav\Common\Flex\Types\Pages\PageIndex;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
+use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Taxonomy;
use Grav\Common\Uri;
use Grav\Common\Utils;
+use Grav\Framework\Flex\Flex;
+use Grav\Framework\Flex\FlexDirectory;
+use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
+use Grav\Framework\Flex\Pages\FlexPageObject;
use Grav\Plugin\Admin;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use SplFileInfo;
+use Symfony\Component\EventDispatcher\EventDispatcher;
use Whoops\Exception\ErrorException;
use Collator;
+use function array_key_exists;
+use function array_search;
+use function count;
+use function dirname;
+use function extension_loaded;
+use function in_array;
+use function is_array;
+use function is_int;
+use function is_string;
+/**
+ * Class Pages
+ * @package Grav\Common\Page
+ */
class Pages
{
- /**
- * @var Grav
- */
- protected $grav;
-
- /**
- * @var array|PageInterface[]
- */
- protected $instances;
+ /** @var FlexDirectory|null */
+ private $directory;
- /**
- * @var array|string[]
- */
+ /** @var Grav */
+ protected $grav;
+ /** @var array */
+ protected $instances = [];
+ /** @var array */
+ protected $index = [];
+ /** @var array */
protected $children;
-
- /**
- * @var string
- */
+ /** @var string */
protected $base = '';
-
- /**
- * @var array|string[]
- */
+ /** @var string[] */
protected $baseRoute = [];
-
- /**
- * @var array|string[]
- */
+ /** @var string[] */
protected $routes = [];
-
- /**
- * @var array
- */
+ /** @var array */
protected $sort;
-
- /**
- * @var Blueprints
- */
+ /** @var Blueprints */
protected $blueprints;
-
- /**
- * @var int
- */
+ /** @var bool */
+ protected $enable_pages = true;
+ /** @var int */
protected $last_modified;
-
- /**
- * @var array|string[]
- */
+ /** @var string[] */
protected $ignore_files;
-
- /**
- * @var array|string[]
- */
+ /** @var string[] */
protected $ignore_folders;
-
- /**
- * @var bool
- */
+ /** @var bool */
protected $ignore_hidden;
-
/** @var string */
protected $check_method;
-
+ /** @var string */
protected $pages_cache_id;
-
+ /** @var bool */
protected $initialized = false;
-
+ /** @var string */
protected $active_lang;
+ /** @var bool */
+ protected $fire_events = false;
+ /** @var Types|null */
+ protected static $types;
+ /** @var string|null */
+ protected static $home_route;
/**
- * @var Types
+ * Constructor
+ *
+ * @param Grav $grav
*/
- static protected $types;
+ public function __construct(Grav $grav)
+ {
+ $this->grav = $grav;
+ }
/**
- * @var string
+ * @return FlexDirectory|null
*/
- static protected $home_route;
+ public function getDirectory(): ?FlexDirectory
+ {
+ return $this->directory;
+ }
/**
- * Constructor
- *
- * @param Grav $c
+ * Method used in admin to disable frontend pages from being initialized.
*/
- public function __construct(Grav $c)
+ public function disablePages(): void
{
- $this->grav = $c;
+ $this->enable_pages = false;
+ }
+
+ /**
+ * Method used in admin to later load frontend pages.
+ */
+ public function enablePages(): void
+ {
+ if (!$this->enable_pages) {
+ $this->enable_pages = true;
+
+ $this->init();
+ }
}
/**
* Get or set base path for the pages.
*
- * @param string $path
- *
+ * @param string|null $path
* @return string
*/
public function base($path = null)
{
if ($path !== null) {
$path = trim($path, '/');
- $this->base = $path ? '/' . $path : null;
+ $this->base = $path ? '/' . $path : '';
$this->baseRoute = [];
}
@@ -139,8 +159,7 @@ class Pages
*
* Get base route for Grav pages.
*
- * @param string $lang Optional language code for multilingual routes.
- *
+ * @param string|null $lang Optional language code for multilingual routes.
* @return string
*/
public function baseRoute($lang = null)
@@ -165,8 +184,7 @@ class Pages
* Get route for Grav site.
*
* @param string $route Optional route to the page.
- * @param string $lang Optional language code for multilingual links.
- *
+ * @param string|null $lang Optional language code for multilingual links.
* @return string
*/
public function route($route = '/', $lang = null)
@@ -178,13 +196,64 @@ class Pages
return $this->baseRoute($lang) . $route;
}
+ /**
+ * Get relative referrer route and language code. Returns null if the route isn't within the current base, language (if set) and route.
+ *
+ * @example `$langCode = null; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within /admin and updates $langCode
+ * @example `$langCode = 'en'; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within the /en/admin
+ *
+ * @param string|null $langCode Variable to store the language code. If already set, check only against that language.
+ * @param string $route Optional route within the site.
+ * @return string|null
+ * @since 1.7.23
+ */
+ public function referrerRoute(?string &$langCode, string $route = '/'): ?string
+ {
+ $referrer = $_SERVER['HTTP_REFERER'] ?? null;
+
+ // Start by checking that referrer came from our site.
+ $root = $this->grav['base_url_absolute'];
+ if (!is_string($referrer) || !str_starts_with($referrer, $root)) {
+ return null;
+ }
+
+ /** @var Language $language */
+ $language = $this->grav['language'];
+
+ // Get all language codes and append no language.
+ if (null === $langCode) {
+ $languages = $language->enabled() ? $language->getLanguages() : [];
+ $languages[] = '';
+ } else {
+ $languages[] = $langCode;
+ }
+
+ $path_base = rtrim($this->base(), '/');
+ $path_route = rtrim($route, '/');
+
+ // Try to figure out the language code.
+ foreach ($languages as $code) {
+ $path_lang = $code ? "/{$code}" : '';
+
+ $base = $path_base . $path_lang . $path_route;
+ if ($referrer === $base || str_starts_with($referrer, "{$base}/")) {
+ if (null === $langCode) {
+ $langCode = $code;
+ }
+
+ return substr($referrer, \strlen($base));
+ }
+ }
+
+ return null;
+ }
+
/**
*
* Get base URL for Grav pages.
*
- * @param string $lang Optional language code for multilingual links.
+ * @param string|null $lang Optional language code for multilingual links.
* @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
- *
* @return string
*/
public function baseUrl($lang = null, $absolute = null)
@@ -204,9 +273,8 @@ class Pages
*
* Get home URL for Grav site.
*
- * @param string $lang Optional language code for multilingual links.
- * @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
- *
+ * @param string|null $lang Optional language code for multilingual links.
+ * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
* @return string
*/
public function homeUrl($lang = null, $absolute = null)
@@ -219,9 +287,8 @@ class Pages
* Get URL for Grav site.
*
* @param string $route Optional route to the page.
- * @param string $lang Optional language code for multilingual links.
- * @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
- *
+ * @param string|null $lang Optional language code for multilingual links.
+ * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
* @return string
*/
public function url($route = '/', $lang = null, $absolute = null)
@@ -233,15 +300,33 @@ class Pages
return $this->baseUrl($lang, $absolute) . Uri::filterPath($route);
}
- public function setCheckMethod($method)
+ /**
+ * @param string $method
+ * @return void
+ */
+ public function setCheckMethod($method): void
{
$this->check_method = strtolower($method);
}
+ /**
+ * @return void
+ */
+ public function register(): void
+ {
+ $config = $this->grav['config'];
+ $type = $config->get('system.pages.type');
+ if ($type === 'flex') {
+ $this->initFlexPages();
+ }
+ }
+
/**
* Reset pages (used in search indexing etc).
+ *
+ * @return void
*/
- public function reset()
+ public function reset(): void
{
$this->initialized = false;
@@ -251,18 +336,20 @@ class Pages
/**
* Class initialization. Must be called before using this class.
*/
- public function init()
+ public function init(): void
{
if ($this->initialized) {
return;
}
$config = $this->grav['config'];
- $this->ignore_files = $config->get('system.pages.ignore_files');
- $this->ignore_folders = $config->get('system.pages.ignore_folders');
- $this->ignore_hidden = $config->get('system.pages.ignore_hidden');
+ $this->ignore_files = (array)$config->get('system.pages.ignore_files');
+ $this->ignore_folders = (array)$config->get('system.pages.ignore_folders');
+ $this->ignore_hidden = (bool)$config->get('system.pages.ignore_hidden');
+ $this->fire_events = (bool)$config->get('system.pages.events.page');
$this->instances = [];
+ $this->index = [];
$this->children = [];
$this->routes = [];
@@ -270,14 +357,22 @@ class Pages
$this->setCheckMethod($config->get('system.cache.check.method', 'file'));
}
+ if ($this->enable_pages === false) {
+ $page = $this->buildRootPage();
+ $this->instances[$page->path()] = $page;
+
+ return;
+ }
+
$this->buildPages();
+
+ $this->initialized = true;
}
/**
* Get or set last modification time.
*
- * @param int $modified
- *
+ * @param int|null $modified
* @return int|null
*/
public function lastModified($modified = null)
@@ -292,11 +387,19 @@ class Pages
/**
* Returns a list of all pages.
*
- * @return array|PageInterface[]
+ * @return PageInterface[]
*/
public function instances()
{
- return $this->instances;
+ $instances = [];
+ foreach ($this->index as $path => $instance) {
+ $page = $this->get($path);
+ if ($page) {
+ $instances[$path] = $page;
+ }
+ }
+
+ return $instances;
}
/**
@@ -313,29 +416,346 @@ class Pages
* Adds a page and assigns a route to it.
*
* @param PageInterface $page Page to be added.
- * @param string $route Optional route (uses route from the object if not set).
+ * @param string|null $route Optional route (uses route from the object if not set).
*/
- public function addPage(PageInterface $page, $route = null)
+ public function addPage(PageInterface $page, $route = null): void
{
- if (!isset($this->instances[$page->path()])) {
- $this->instances[$page->path()] = $page;
+ $path = $page->path() ?? '';
+ if (!isset($this->index[$path])) {
+ $this->index[$path] = $page;
+ $this->instances[$path] = $page;
}
$route = $page->route($route);
- if ($page->parent()) {
- $this->children[$page->parent()->path()][$page->path()] = ['slug' => $page->slug()];
+ $parent = $page->parent();
+ if ($parent) {
+ $this->children[$parent->path() ?? ''][$path] = ['slug' => $page->slug()];
}
- $this->routes[$route] = $page->path();
+ $this->routes[$route] = $path;
$this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
}
+ /**
+ * Get a collection of pages in the given context.
+ *
+ * @param array $params
+ * @param array $context
+ * @return PageCollectionInterface|Collection
+ */
+ public function getCollection(array $params = [], array $context = [])
+ {
+ if (!isset($params['items'])) {
+ return new Collection();
+ }
+
+ /** @var Config $config */
+ $config = $this->grav['config'];
+
+ $context += [
+ 'event' => true,
+ 'pagination' => true,
+ 'url_taxonomy_filters' => $config->get('system.pages.url_taxonomy_filters'),
+ 'taxonomies' => (array)$config->get('site.taxonomies'),
+ 'pagination_page' => 1,
+ 'self' => null,
+ ];
+
+ // Include taxonomies from the URL if requested.
+ $process_taxonomy = $params['url_taxonomy_filters'] ?? $context['url_taxonomy_filters'];
+ if ($process_taxonomy) {
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+ foreach ($context['taxonomies'] as $taxonomy) {
+ $param = $uri->param(rawurlencode($taxonomy));
+ $items = is_string($param) ? explode(',', $param) : [];
+ foreach ($items as $item) {
+ $params['taxonomies'][$taxonomy][] = htmlspecialchars_decode(rawurldecode($item), ENT_QUOTES);
+ }
+ }
+ }
+
+ $pagination = $params['pagination'] ?? $context['pagination'];
+ if ($pagination && !isset($params['page'], $params['start'])) {
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+ $context['current_page'] = $uri->currentPage();
+ }
+
+ $collection = $this->evaluate($params['items'], $context['self']);
+ $collection->setParams($params);
+
+ // Filter by taxonomies.
+ foreach ($params['taxonomies'] ?? [] as $taxonomy => $items) {
+ foreach ($collection as $page) {
+ // Don't include modules
+ if ($page->isModule()) {
+ continue;
+ }
+
+ $test = $page->taxonomy()[$taxonomy] ?? [];
+ foreach ($items as $item) {
+ if (!$test || !in_array($item, $test, true)) {
+ $collection->remove($page->path());
+ }
+ }
+ }
+ }
+
+ $filters = $params['filter'] ?? [];
+
+ // Assume published=true if not set.
+ if (!isset($filters['published']) && !isset($filters['non-published'])) {
+ $filters['published'] = true;
+ }
+
+ // Remove any inclusive sets from filter.
+ $sets = ['published', 'visible', 'modular', 'routable'];
+ foreach ($sets as $type) {
+ $nonType = "non-{$type}";
+ if (isset($filters[$type], $filters[$nonType]) && $filters[$type] === $filters[$nonType]) {
+ if (!$filters[$type]) {
+ // Both options are false, return empty collection as nothing can match the filters.
+ return new Collection();
+ }
+
+ // Both options are true, remove opposite filters as all pages will match the filters.
+ unset($filters[$type], $filters[$nonType]);
+ }
+ }
+
+ // Filter the collection
+ foreach ($filters as $type => $filter) {
+ if (null === $filter) {
+ continue;
+ }
+
+ // Convert non-type to type.
+ if (str_starts_with($type, 'non-')) {
+ $type = substr($type, 4);
+ $filter = !$filter;
+ }
+
+ switch ($type) {
+ case 'translated':
+ if ($filter) {
+ $collection = $collection->translated();
+ } else {
+ $collection = $collection->nonTranslated();
+ }
+ break;
+ case 'published':
+ if ($filter) {
+ $collection = $collection->published();
+ } else {
+ $collection = $collection->nonPublished();
+ }
+ break;
+ case 'visible':
+ if ($filter) {
+ $collection = $collection->visible();
+ } else {
+ $collection = $collection->nonVisible();
+ }
+ break;
+ case 'page':
+ if ($filter) {
+ $collection = $collection->pages();
+ } else {
+ $collection = $collection->modules();
+ }
+ break;
+ case 'module':
+ case 'modular':
+ if ($filter) {
+ $collection = $collection->modules();
+ } else {
+ $collection = $collection->pages();
+ }
+ break;
+ case 'routable':
+ if ($filter) {
+ $collection = $collection->routable();
+ } else {
+ $collection = $collection->nonRoutable();
+ }
+ break;
+ case 'type':
+ $collection = $collection->ofType($filter);
+ break;
+ case 'types':
+ $collection = $collection->ofOneOfTheseTypes($filter);
+ break;
+ case 'access':
+ $collection = $collection->ofOneOfTheseAccessLevels($filter);
+ break;
+ }
+ }
+
+ if (isset($params['dateRange'])) {
+ $start = $params['dateRange']['start'] ?? null;
+ $end = $params['dateRange']['end'] ?? null;
+ $field = $params['dateRange']['field'] ?? null;
+ $collection = $collection->dateRange($start, $end, $field);
+ }
+
+ if (isset($params['order'])) {
+ $by = $params['order']['by'] ?? 'default';
+ $dir = $params['order']['dir'] ?? 'asc';
+ $custom = $params['order']['custom'] ?? null;
+ $sort_flags = $params['order']['sort_flags'] ?? null;
+
+ if (is_array($sort_flags)) {
+ $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
+ $sort_flags = array_reduce($sort_flags, static function ($a, $b) {
+ return $a | $b;
+ }, 0); //merge constant values using bit or
+ }
+
+ $collection = $collection->order($by, $dir, $custom, $sort_flags);
+ }
+
+ // New Custom event to handle things like pagination.
+ if ($context['event']) {
+ $this->grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection, 'context' => $context]));
+ }
+
+ if ($context['pagination']) {
+ // Slice and dice the collection if pagination is required
+ $params = $collection->params();
+
+ $limit = (int)($params['limit'] ?? 0);
+ $page = (int)($params['page'] ?? $context['current_page'] ?? 0);
+ $start = (int)($params['start'] ?? 0);
+ $start = $limit > 0 && $page > 0 ? ($page - 1) * $limit : max(0, $start);
+
+ if ($start || ($limit && $collection->count() > $limit)) {
+ $collection->slice($start, $limit ?: null);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * @param array|string $value
+ * @param PageInterface|null $self
+ * @return Collection
+ */
+ protected function evaluate($value, PageInterface $self = null)
+ {
+ // Parse command.
+ if (is_string($value)) {
+ // Format: @command.param
+ $cmd = $value;
+ $params = [];
+ } elseif (is_array($value) && count($value) === 1 && !is_int(key($value))) {
+ // Format: @command.param: { attr1: value1, attr2: value2 }
+ $cmd = (string)key($value);
+ $params = (array)current($value);
+ } else {
+ $result = [];
+ foreach ((array)$value as $key => $val) {
+ if (is_int($key)) {
+ $result = $result + $this->evaluate($val, $self)->toArray();
+ } else {
+ $result = $result + $this->evaluate([$key => $val], $self)->toArray();
+ }
+ }
+
+ return new Collection($result);
+ }
+
+ $parts = explode('.', $cmd);
+ $scope = array_shift($parts);
+ $type = $parts[0] ?? null;
+
+ /** @var PageInterface|null $page */
+ $page = null;
+ switch ($scope) {
+ case 'self@':
+ case '@self':
+ $page = $self;
+ break;
+
+ case 'page@':
+ case '@page':
+ $page = isset($params[0]) ? $this->find($params[0]) : null;
+ break;
+
+ case 'root@':
+ case '@root':
+ $page = $this->root();
+ break;
+
+ case 'taxonomy@':
+ case '@taxonomy':
+ // Gets a collection of pages by using one of the following formats:
+ // @taxonomy.category: blog
+ // @taxonomy.category: [ blog, featured ]
+ // @taxonomy: { category: [ blog, featured ], level: 1 }
+
+ /** @var Taxonomy $taxonomy_map */
+ $taxonomy_map = Grav::instance()['taxonomy'];
+
+ if (!empty($parts)) {
+ $params = [implode('.', $parts) => $params];
+ }
+
+ return $taxonomy_map->findTaxonomy($params);
+ }
+
+ if (!$page) {
+ return new Collection();
+ }
+
+ // Handle '@page', '@page.modular: false', '@self' and '@self.modular: false'.
+ if (null === $type || (in_array($type, ['modular', 'modules']) && ($params[0] ?? null) === false)) {
+ $type = 'children';
+ }
+
+ switch ($type) {
+ case 'all':
+ $collection = $page->children();
+ break;
+ case 'modules':
+ case 'modular':
+ $collection = $page->children()->modules();
+ break;
+ case 'pages':
+ case 'children':
+ $collection = $page->children()->pages();
+ break;
+ case 'page':
+ case 'self':
+ $collection = !$page->root() ? (new Collection())->addPage($page) : new Collection();
+ break;
+ case 'parent':
+ $parent = $page->parent();
+ $collection = new Collection();
+ $collection = $parent ? $collection->addPage($parent) : $collection;
+ break;
+ case 'siblings':
+ $parent = $page->parent();
+ $collection = $parent ? $parent->children()->remove($page->path()) : new Collection();
+ break;
+ case 'descendants':
+ $collection = $this->all($page)->remove($page->path())->pages();
+ break;
+ default:
+ // Unknown type; return empty collection.
+ $collection = new Collection();
+ break;
+ }
+
+ return $collection;
+ }
+
/**
* Sort sub-pages in a page.
*
* @param PageInterface $page
- * @param string $order_by
- * @param string $order_dir
- *
+ * @param string|null $order_by
+ * @param string|null $order_dir
* @return array
*/
public function sort(PageInterface $page, $order_by = null, $order_dir = null, $sort_flags = null)
@@ -348,6 +768,10 @@ class Pages
}
$path = $page->path();
+ if (null === $path) {
+ return [];
+ }
+
$children = $this->children[$path] ?? [];
if (!$children) {
@@ -369,11 +793,10 @@ class Pages
/**
* @param Collection $collection
- * @param string|int $orderBy
+ * @param string $orderBy
* @param string $orderDir
* @param array|null $orderManual
* @param int|null $sort_flags
- *
* @return array
* @internal
*/
@@ -396,27 +819,70 @@ class Pages
}
return $sort;
-
}
/**
* Get a page instance.
*
* @param string $path The filesystem full path of the page
- *
- * @return PageInterface
- * @throws \Exception
+ * @return PageInterface|null
+ * @throws RuntimeException
*/
public function get($path)
{
- return $this->instances[(string)$path] ?? null;
+ $path = (string)$path;
+ if ($path === '') {
+ return null;
+ }
+
+ // Check for local instances first.
+ if (array_key_exists($path, $this->instances)) {
+ return $this->instances[$path];
+ }
+
+ $instance = $this->index[$path] ?? null;
+ if (is_string($instance)) {
+ if ($this->directory) {
+ /** @var Language $language */
+ $language = $this->grav['language'];
+ $lang = $language->getActive();
+ if ($lang) {
+ $languages = $language->getFallbackLanguages($lang, true);
+ $key = $instance;
+ $instance = null;
+ foreach ($languages as $code) {
+ $test = $code ? $key . ':' . $code : $key;
+ if (($instance = $this->directory->getObject($test, 'flex_key')) !== null) {
+ break;
+ }
+ }
+ } else {
+ $instance = $this->directory->getObject($instance, 'flex_key');
+ }
+ }
+
+ if ($instance instanceof PageInterface) {
+ if ($this->fire_events && method_exists($instance, 'initialize')) {
+ $instance->initialize();
+ }
+ } else {
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage(sprintf('Flex page %s is missing or broken!', $instance), 'debug');
+ }
+ }
+
+ if ($instance) {
+ $this->instances[$path] = $instance;
+ }
+
+ return $instance;
}
/**
* Get children of the path.
*
* @param string $path
- *
* @return Collection
*/
public function children($path)
@@ -430,14 +896,13 @@ class Pages
* Get a page ancestor.
*
* @param string $route The relative URL of the page
- * @param string $path The relative path of the ancestor folder
- *
+ * @param string|null $path The relative path of the ancestor folder
* @return PageInterface|null
*/
public function ancestor($route, $path = null)
{
if ($path !== null) {
- $page = $this->dispatch($route, true);
+ $page = $this->find($route, true);
if ($page && $page->path() === $path) {
return $page;
@@ -456,15 +921,13 @@ class Pages
* Get a page ancestor trait.
*
* @param string $route The relative route of the page
- * @param string $field The field name of the ancestor to query for
- *
+ * @param string|null $field The field name of the ancestor to query for
* @return PageInterface|null
*/
public function inherited($route, $field = null)
{
if ($field !== null) {
-
- $page = $this->dispatch($route, true);
+ $page = $this->find($route, true);
$parent = $page ? $page->parent() : null;
if ($parent && $parent->value('header.' . $field) !== null) {
@@ -479,97 +942,146 @@ class Pages
}
/**
- * alias method to return find a page.
- *
- * @param string $route The relative URL of the page
- * @param bool $all
+ * Find a page based on route.
*
+ * @param string $route The route of the page
+ * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable
* @return PageInterface|null
*/
public function find($route, $all = false)
{
- return $this->dispatch($route, $all, false);
+ $route = urldecode((string)$route);
+
+ // Fetch page if there's a defined route to it.
+ $path = $this->routes[$route] ?? null;
+ $page = null !== $path ? $this->get($path) : null;
+
+ // Try without trailing slash
+ if (null === $page && Utils::endsWith($route, '/')) {
+ $path = $this->routes[rtrim($route, '/')] ?? null;
+ $page = null !== $path ? $this->get($path) : null;
+ }
+
+ if (!$all && !isset($this->grav['admin'])) {
+ if (null === $page || !$page->routable()) {
+ // If the page cannot be accessed, look for the site wide routes and wildcards.
+ $page = $this->findSiteBasedRoute($route) ?? $page;
+ }
+ }
+
+ return $page;
+ }
+
+ /**
+ * Check site based routes.
+ *
+ * @param string $route
+ * @return PageInterface|null
+ */
+ protected function findSiteBasedRoute($route)
+ {
+ /** @var Config $config */
+ $config = $this->grav['config'];
+
+ $site_routes = $config->get('site.routes');
+ if (!is_array($site_routes)) {
+ return null;
+ }
+
+ $page = null;
+
+ // See if route matches one in the site configuration
+ $site_route = $site_routes[$route] ?? null;
+ if ($site_route) {
+ $page = $this->find($site_route);
+ } else {
+ // Use reverse order because of B/C (previously matched multiple and returned the last match).
+ foreach (array_reverse($site_routes, true) as $pattern => $replace) {
+ $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
+ try {
+ $found = preg_replace($pattern, $replace, $route);
+ if ($found && $found !== $route) {
+ $page = $this->find($found);
+ if ($page) {
+ return $page;
+ }
+ }
+ } catch (ErrorException $e) {
+ $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
+ }
+ }
+ }
+
+ return $page;
}
/**
* Dispatch URI to a page.
*
* @param string $route The relative URL of the page
- * @param bool $all
- *
- * @param bool $redirect
+ * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable
+ * @param bool $redirect If true, allow redirects
* @return PageInterface|null
- * @throws \Exception
+ * @throws Exception
*/
public function dispatch($route, $all = false, $redirect = true)
{
- $route = urldecode($route);
+ $page = $this->find($route, true);
- // Fetch page if there's a defined route to it.
- $page = isset($this->routes[$route]) ? $this->get($this->routes[$route]) : null;
- // Try without trailing slash
- if (!$page && Utils::endsWith($route, '/')) {
- $page = isset($this->routes[rtrim($route, '/')]) ? $this->get($this->routes[rtrim($route, '/')]) : null;
+ // If we want all pages or are in admin, return what we already have.
+ if ($all || isset($this->grav['admin'])) {
+ return $page;
}
- // Are we in the admin? this is important!
- $not_admin = !isset($this->grav['admin']);
+ if ($page) {
+ $routable = $page->routable();
+ if ($redirect) {
+ if ($page->redirect()) {
+ // Follow a redirect page.
+ $this->grav->redirectLangSafe($page->redirect());
+ }
- // If the page cannot be reached, look into site wide redirects, routes + wildcards
- if (!$all && $not_admin) {
+ if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) {
+ // Redirect to the first visible child as current page isn't routable.
+ $this->grav->redirectLangSafe($child->route());
+ }
+ }
- // If the page is a simple redirect, just do it.
- if ($redirect && $page && $page->redirect()) {
- $this->grav->redirectLangSafe($page->redirect());
+ if ($routable) {
+ return $page;
}
+ }
- // fall back and check site based redirects
- if (!$page || ($page && !$page->routable())) {
- /** @var Config $config */
- $config = $this->grav['config'];
+ $route = urldecode((string)$route);
- // See if route matches one in the site configuration
- $site_route = $config->get("site.routes.{$route}");
- if ($site_route) {
- $page = $this->dispatch($site_route, $all);
- } else {
+ // The page cannot be reached, look into site wide redirects, routes and wildcards.
+ $redirectedPage = $this->findSiteBasedRoute($route);
+ if ($redirectedPage) {
+ $page = $this->dispatch($redirectedPage->route(), false, $redirect);
+ }
- /** @var Uri $uri */
- $uri = $this->grav['uri'];
- /** @var \Grav\Framework\Uri\Uri $source_url */
- $source_url = $uri->uri(false);
-
- // Try Regex style redirects
- $site_redirects = $config->get('site.redirects');
- if (is_array($site_redirects)) {
- foreach ((array)$site_redirects as $pattern => $replace) {
- $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
- try {
- $found = preg_replace($pattern, $replace, $source_url);
- if ($found !== $source_url) {
- $this->grav->redirectLangSafe($found);
- }
- } catch (ErrorException $e) {
- $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
- }
- }
- }
+ /** @var Config $config */
+ $config = $this->grav['config'];
- // Try Regex style routes
- $site_routes = $config->get('site.routes');
- if (is_array($site_routes)) {
- foreach ((array)$site_routes as $pattern => $replace) {
- $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
- try {
- $found = preg_replace($pattern, $replace, $source_url);
- if ($found !== $source_url) {
- $page = $this->dispatch($found, $all);
- }
- } catch (ErrorException $e) {
- $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
- }
- }
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+ /** @var \Grav\Framework\Uri\Uri $source_url */
+ $source_url = $uri->uri(false);
+
+ // Try Regex style redirects
+ $site_redirects = $config->get('site.redirects');
+ if (is_array($site_redirects)) {
+ foreach ((array)$site_redirects as $pattern => $replace) {
+ $pattern = ltrim($pattern, '^');
+ $pattern = '#^' . str_replace('/', '\/', $pattern) . '#';
+ try {
+ /** @var string $found */
+ $found = preg_replace($pattern, $replace, $source_url);
+ if ($found && $found !== $source_url) {
+ $this->grav->redirectLangSafe($found);
}
+ } catch (ErrorException $e) {
+ $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
}
}
}
@@ -581,20 +1093,26 @@ class Pages
* Get root page.
*
* @return PageInterface
+ * @throws RuntimeException
*/
public function root()
{
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
- return $this->instances[rtrim($locator->findResource('page://'), DS)];
+ $path = $locator->findResource('page://');
+ $root = is_string($path) ? $this->get(rtrim($path, '/')) : null;
+ if (null === $root) {
+ throw new RuntimeException('Internal error');
+ }
+
+ return $root;
}
/**
* Get a blueprint for a page type.
*
* @param string $type
- *
* @return Blueprint
*/
public function blueprints($type)
@@ -605,13 +1123,13 @@ class Pages
try {
$blueprint = $this->blueprints->get($type);
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
$blueprint = $this->blueprints->get('default');
}
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;
@@ -620,9 +1138,8 @@ class Pages
/**
* Get all pages
*
- * @param PageInterface $current
- *
- * @return \Grav\Common\Page\Collection
+ * @param PageInterface|null $current
+ * @return Collection
*/
public function all(PageInterface $current = null)
{
@@ -658,7 +1175,6 @@ class Pages
* Get available parents routes
*
* @param bool $rawRoutes get the raw route or the normal route
- *
* @return array
*/
private static function getParents($rawRoutes)
@@ -681,19 +1197,17 @@ class Pages
if (isset($parents[$page_route])) {
unset($parents[$page_route]);
}
-
}
return $parents;
}
/**
- * Get list of route/title of all pages.
+ * Get list of route/title of all pages. Title is in HTML.
*
- * @param PageInterface $current
+ * @param PageInterface|null $current
* @param int $level
* @param bool $rawRoutes
- *
* @param bool $showAll
* @param bool $showFullpath
* @param bool $showSlug
@@ -705,7 +1219,7 @@ class Pages
{
if (!$current) {
if ($level) {
- throw new \RuntimeException('Internal error');
+ throw new RuntimeException('Internal error');
}
$current = $this->root();
@@ -721,22 +1235,18 @@ class Pages
}
if ($showFullpath) {
- $option = $current->route();
+ $option = htmlspecialchars($current->route());
} else {
$extra = $showSlug ? '(' . $current->slug() . ') ' : '';
- $option = str_repeat('—-', $level). '▸ ' . $extra . $current->title();
-
-
+ $option = str_repeat('—-', $level). '▸ ' . $extra . htmlspecialchars($current->title());
}
$list[$route] = $option;
-
-
}
if ($limitLevels === false || ($level+1 < $limitLevels)) {
foreach ($current->children() as $next) {
- if ($showAll || $next->routable() || ($next->modular() && $showModular)) {
+ if ($showAll || $next->routable() || ($next->isModule() && $showModular)) {
$list = array_merge($list, $this->getList($next, $level + 1, $rawRoutes, $showAll, $showFullpath, $showSlug, $showModular, $limitLevels));
}
}
@@ -752,23 +1262,38 @@ class Pages
*/
public static function getTypes()
{
- if (!self::$types) {
+ if (null === self::$types) {
$grav = Grav::instance();
- $scanBlueprintsAndTemplates = function () use ($grav) {
+ /** @var UniformResourceLocator $locator */
+ $locator = $grav['locator'];
+
+ // Prevent calls made before theme:// has been initialized (happens when upgrading old version of Admin plugin).
+ if (!$locator->isStream('theme://')) {
+ return new Types();
+ }
+
+ $scanBlueprintsAndTemplates = static function (Types $types) use ($grav) {
// Scan blueprints
$event = new Event();
- $event->types = self::$types;
+ $event->types = $types;
$grav->fireEvent('onGetPageBlueprints', $event);
- self::$types->scanBlueprints('theme://blueprints/');
+ $types->init();
+
+ // Try new location first.
+ $lookup = 'theme://blueprints/pages/';
+ if (!is_dir($lookup)) {
+ $lookup = 'theme://blueprints/';
+ }
+ $types->scanBlueprints($lookup);
// Scan templates
$event = new Event();
- $event->types = self::$types;
+ $event->types = $types;
$grav->fireEvent('onGetPageTemplates', $event);
- self::$types->scanTemplates('theme://templates/');
+ $types->scanTemplates('theme://templates/');
};
if ($grav['config']->get('system.cache.enabled')) {
@@ -777,22 +1302,21 @@ class Pages
// Use cached types if possible.
$types_cache_id = md5('types');
- self::$types = $cache->fetch($types_cache_id);
+ $types = $cache->fetch($types_cache_id);
- if (!self::$types) {
- self::$types = new Types();
- $scanBlueprintsAndTemplates();
- $cache->save($types_cache_id, self::$types);
+ if (!$types instanceof Types) {
+ $types = new Types();
+ $scanBlueprintsAndTemplates($types);
+ $cache->save($types_cache_id, $types);
}
-
} else {
- self::$types = new Types();
- $scanBlueprintsAndTemplates();
+ $types = new Types();
+ $scanBlueprintsAndTemplates($types);
}
// Register custom paths to the locator.
$locator = $grav['locator'];
- foreach (self::$types as $type => $paths) {
+ foreach ($types as $type => $paths) {
foreach ($paths as $k => $path) {
if (strpos($path, 'blueprints://') === 0) {
unset($paths[$k]);
@@ -802,6 +1326,8 @@ class Pages
$locator->addPath('blueprints', "pages/$type.yaml", $paths);
}
}
+
+ self::$types = $types;
}
return self::$types;
@@ -834,22 +1360,26 @@ class Pages
/**
* Get template types based on page type (standard or modular)
*
+ * @param string|null $type
* @return array
*/
- public static function pageTypes()
+ public static function pageTypes($type = null)
{
- if (isset(Grav::instance()['admin'])) {
+ if (null === $type && isset(Grav::instance()['admin'])) {
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
- /** @var PageInterface $page */
- $page = $admin->getPage($admin->route);
+ /** @var PageInterface|null $page */
+ $page = $admin->page();
- if ($page && $page->modular()) {
- return static::modularTypes();
- }
+ $type = $page && $page->isModule() ? 'modular' : 'standard';
+ }
- return static::types();
+ switch ($type) {
+ case 'standard':
+ return static::types();
+ case 'modular':
+ return static::modularTypes();
}
return [];
@@ -864,10 +1394,10 @@ class Pages
{
$accessLevels = [];
foreach ($this->all() as $page) {
- if (isset($page->header()->access)) {
- if (\is_array($page->header()->access)) {
+ if ($page instanceof PageInterface && isset($page->header()->access)) {
+ if (is_array($page->header()->access)) {
foreach ($page->header()->access as $index => $accessLevel) {
- if (\is_array($accessLevel)) {
+ if (is_array($accessLevel)) {
foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
$accessLevels[] = $innerIndex;
}
@@ -896,8 +1426,6 @@ class Pages
return self::getParents($rawRoutes);
}
-
-
/**
* Gets the home route
*
@@ -931,7 +1459,6 @@ class Pages
} catch (ErrorException $e) {
$home = $home_aliases[$default];
}
-
}
}
@@ -943,41 +1470,248 @@ class Pages
/**
* Needed for testing where we change the home route via config
+ *
+ * @return string|null
*/
public static function resetHomeRoute()
{
self::$home_route = null;
+
return self::getHomeRoute();
}
+ protected function initFlexPages(): void
+ {
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage('Pages: Flex Directory');
+
+ /** @var Flex $flex */
+ $flex = $this->grav['flex'];
+ $directory = $flex->getDirectory('pages');
+
+ /** @var EventDispatcher $dispatcher */
+ $dispatcher = $this->grav['events'];
+
+ // Stop /admin/pages from working, display error instead.
+ $dispatcher->addListener(
+ 'onAdminPage',
+ static function (Event $event) use ($directory) {
+ $grav = Grav::instance();
+ $admin = $grav['admin'];
+ [$base,$location,] = $admin->getRouteDetails();
+ if ($location !== 'pages' || isset($grav['flex_objects'])) {
+ return;
+ }
+
+ /** @var PageInterface $page */
+ $page = $event['page'];
+ $page->init(new SplFileInfo('plugin://admin/pages/admin/error.md'));
+ $page->routable(true);
+ $header = $page->header();
+ $header->title = 'Please install missing plugin';
+ $page->content("## Please install and enable **[Flex Objects]({$base}/plugins/flex-objects)** plugin. It is required to edit **Flex Pages**.");
+
+ /** @var Header $header */
+ $header = $page->header();
+ $menu = $directory->getConfig('admin.menu.list');
+ $header->access = $menu['authorize'] ?? ['admin.super'];
+ },
+ 100000
+ );
+
+ $this->directory = $directory;
+ }
+
/**
* Builds pages.
*
* @internal
*/
- protected function buildPages()
+ protected function buildPages(): void
{
- $this->sort = [];
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->startTimer('build-pages', 'Init frontend routes');
+
+ if ($this->directory) {
+ $this->buildFlexPages($this->directory);
+ } else {
+ $this->buildRegularPages();
+ }
+ $debugger->stopTimer('build-pages');
+ }
+ protected function buildFlexPages(FlexDirectory $directory): void
+ {
/** @var Config $config */
$config = $this->grav['config'];
+ // TODO: right now we are just emulating normal pages, it is inefficient and bad... but works!
+ /** @var PageCollection|PageIndex $collection */
+ $collection = $directory->getIndex(null, 'storage_key');
+ $cache = $directory->getCache('index');
+
/** @var Language $language */
$language = $this->grav['language'];
+ $this->pages_cache_id = 'pages-' . md5($collection->getCacheChecksum() . $language->getActive() . $config->checksum());
+
+ $cached = $cache->get($this->pages_cache_id);
+
+ if ($cached && $this->getVersion() === $cached[0]) {
+ [, $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached;
+
+ /** @var Taxonomy $taxonomy */
+ $taxonomy = $this->grav['taxonomy'];
+ $taxonomy->taxonomy($taxonomy_map);
+
+ return;
+ }
+
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage('Page cache missed, rebuilding Flex Pages..');
+
+ $root = $collection->getRoot();
+ $root_path = $root->path();
+ $this->routes = [];
+ $this->instances = [$root_path => $root];
+ $this->index = [$root_path => $root];
+ $this->children = [];
+ $this->sort = [];
+
+ if ($this->fire_events) {
+ $this->grav->fireEvent('onBuildPagesInitialized');
+ }
+
+ /** @var PageInterface $page */
+ foreach ($collection as $page) {
+ $path = $page->path();
+ if (null === $path) {
+ throw new RuntimeException('Internal error');
+ }
+
+ if ($page instanceof FlexTranslateInterface) {
+ $page = $page->hasTranslation() ? $page->getTranslation() : null;
+ }
+
+ if (!$page instanceof FlexPageObject || $path === $root_path) {
+ continue;
+ }
+
+ if ($this->fire_events) {
+ if (method_exists($page, 'initialize')) {
+ $page->initialize();
+ } else {
+ // TODO: Deprecated, only used in 1.7 betas.
+ $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
+ }
+ }
+
+ $parent = dirname($path);
+
+ $route = $page->rawRoute();
+
+ // Skip duplicated empty folders (git revert does not remove those).
+ // TODO: still not perfect, will only work if the page has been translated.
+ if (isset($this->routes[$route])) {
+ $oldPath = $this->routes[$route];
+ if ($page->isPage()) {
+ unset($this->index[$oldPath], $this->children[dirname($oldPath)][$oldPath]);
+ } else {
+ continue;
+ }
+ }
+
+ $this->routes[$route] = $path;
+ $this->instances[$path] = $page;
+ $this->index[$path] = $page->getFlexKey();
+ // FIXME: ... better...
+ $this->children[$parent][$path] = ['slug' => $page->slug()];
+ if (!isset($this->children[$path])) {
+ $this->children[$path] = [];
+ }
+ }
+
+ foreach ($this->children as $path => $list) {
+ $page = $this->instances[$path] ?? null;
+ if (null === $page) {
+ continue;
+ }
+ // Call onFolderProcessed event.
+ if ($this->fire_events) {
+ $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
+ }
+ // Sort the children.
+ $this->children[$path] = $this->sort($page);
+ }
+
+ $this->routes = [];
+ $this->buildRoutes();
+
+ // cache if needed
+ if (null !== $cache) {
+ /** @var Taxonomy $taxonomy */
+ $taxonomy = $this->grav['taxonomy'];
+ $taxonomy_map = $taxonomy->taxonomy();
+
+ // save pages, routes, taxonomy, and sort to cache
+ $cache->set($this->pages_cache_id, [$this->getVersion(), $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort]);
+ }
+ }
+
+ /**
+ * @return Page
+ */
+ protected function buildRootPage()
+ {
+ $grav = Grav::instance();
+
+ /** @var UniformResourceLocator $locator */
+ $locator = $grav['locator'];
+ $path = $locator->findResource('page://');
+ if (!is_string($path)) {
+ throw new RuntimeException('Internal Error');
+ }
+
+ /** @var Config $config */
+ $config = $grav['config'];
+
+ $page = new Page();
+ $page->path($path);
+ $page->orderDir($config->get('system.pages.order.dir'));
+ $page->orderBy($config->get('system.pages.order.by'));
+ $page->modified(0);
+ $page->routable(false);
+ $page->template('default');
+ $page->extension('.md');
+
+ return $page;
+ }
+
+ protected function buildRegularPages(): void
+ {
+ /** @var Config $config */
+ $config = $this->grav['config'];
+
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
+ /** @var Language $language */
+ $language = $this->grav['language'];
+
$pages_dir = $locator->findResource('page://');
+ if (!is_string($pages_dir)) {
+ throw new RuntimeException('Internal Error');
+ }
// Set active language
$this->active_lang = $language->getActive();
if ($config->get('system.cache.enabled')) {
- /** @var Cache $cache */
- $cache = $this->grav['cache'];
- /** @var Taxonomy $taxonomy */
- $taxonomy = $this->grav['taxonomy'];
+ /** @var Language $language */
+ $language = $this->grav['language'];
// how should we check for last modified? Default is by file
switch ($this->check_method) {
@@ -997,24 +1731,25 @@ class Pages
$this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
+ /** @var Cache $cache */
+ $cache = $this->grav['cache'];
$cached = $cache->fetch($this->pages_cache_id);
- if ($cached) {
- $this->grav['debugger']->addMessage('Page cache hit.');
+ if ($cached && $this->getVersion() === $cached[0]) {
+ [, $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached;
- list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cached;
-
- // If pages was found in cache, set the taxonomy
+ /** @var Taxonomy $taxonomy */
+ $taxonomy = $this->grav['taxonomy'];
$taxonomy->taxonomy($taxonomy_map);
- } else {
- $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
- // recurse pages and cache result
- $this->resetPages($pages_dir);
+ return;
}
+
+ $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
} else {
- $this->recurse($pages_dir);
- $this->buildRoutes();
+ $this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..');
}
+
+ $this->resetPages($pages_dir);
}
/**
@@ -1022,8 +1757,9 @@ class Pages
*
* @param string $pages_dir
*/
- public function resetPages($pages_dir)
+ public function resetPages($pages_dir): void
{
+ $this->sort = [];
$this->recurse($pages_dir);
$this->buildRoutes();
@@ -1035,7 +1771,7 @@ class Pages
$taxonomy = $this->grav['taxonomy'];
// save pages, routes, taxonomy, and sort to cache
- $cache->save($this->pages_cache_id, [$this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]);
+ $cache->save($this->pages_cache_id, [$this->getVersion(), $this->index, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]);
}
}
@@ -1044,9 +1780,8 @@ class Pages
*
* @param string $directory
* @param PageInterface|null $parent
- *
* @return PageInterface
- * @throws \RuntimeException
+ * @throws RuntimeException
* @internal
*/
protected function recurse($directory, PageInterface $parent = null)
@@ -1062,7 +1797,7 @@ class Pages
// Stuff to do at root page
// Fire event for memory and time consuming plugins...
- if ($parent === null && $config->get('system.pages.events.page')) {
+ if ($parent === null && $this->fire_events) {
$this->grav->fireEvent('onBuildPagesInitialized');
}
@@ -1075,19 +1810,20 @@ class Pages
$page->orderBy($config->get('system.pages.order.by'));
// Add into instances
- if (!isset($this->instances[$page->path()])) {
+ if (!isset($this->index[$page->path()])) {
+ $this->index[$page->path()] = $page;
$this->instances[$page->path()] = $page;
if ($parent && $page->path()) {
$this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()];
}
- } else {
- throw new \RuntimeException('Fatal error when creating page instances.');
+ } elseif ($parent !== null) {
+ throw new RuntimeException('Fatal error when creating page instances.');
}
// Build regular expression for all the allowed page extensions.
$page_extensions = $language->getFallbackPageExtensions();
$regex = '/^[^\.]*(' . implode('|', array_map(
- function ($str) {
+ static function ($str) {
return preg_quote($str, '/');
},
$page_extensions
@@ -1098,8 +1834,7 @@ class Pages
$page_extension = '.md';
$last_modified = 0;
- $iterator = new \FilesystemIterator($directory);
- /** @var \FilesystemIterator $file */
+ $iterator = new FilesystemIterator($directory);
foreach ($iterator as $file) {
$filename = $file->getFilename();
@@ -1111,14 +1846,14 @@ class Pages
// Handle folders later.
if ($file->isDir()) {
// But ignore all folders in ignore list.
- if (!\in_array($filename, $this->ignore_folders, true)) {
+ if (!in_array($filename, $this->ignore_folders, true)) {
$folders[] = $file;
}
continue;
}
// Ignore all files in ignore list.
- if (\in_array($filename, $this->ignore_files, true)) {
+ if (in_array($filename, $this->ignore_files, true)) {
continue;
}
@@ -1145,13 +1880,13 @@ class Pages
$content_exists = true;
- if ($config->get('system.pages.events.page')) {
+ if ($this->fire_events) {
$this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
}
}
// Now handle all the folders under the page.
- /** @var \FilesystemIterator $file */
+ /** @var FilesystemIterator $file */
foreach ($folders as $file) {
$filename = $file->getFilename();
@@ -1167,20 +1902,20 @@ class Pages
$path = $directory . DS . $filename;
$child = $this->recurse($path, $page);
- if (Utils::startsWith($filename, '_')) {
+ if (preg_match('/^(\d+\.)_/', $filename)) {
$child->routable(false);
+ $child->modularTwig(true);
}
$this->children[$page->path()][$child->path()] = ['slug' => $child->slug()];
- if ($config->get('system.pages.events.page')) {
+ if ($this->fire_events) {
$this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
}
}
-
if (!$content_exists) {
- // Set routability to false if no page found
+ // Set routable to false if no page found
$page->routable(false);
// Hide empty folders if option set
@@ -1202,7 +1937,7 @@ class Pages
// Override the modified and ID so that it takes the latest change into account
$page->modified($last_modified);
- $page->id($last_modified . md5($page->filePath()));
+ $page->id($last_modified . md5($page->filePath() ?? ''));
// Sort based on Defaults or Page Overridden sort order
$this->children[$page->path()] = $this->sort($page);
@@ -1213,54 +1948,68 @@ class Pages
/**
* @internal
*/
- protected function buildRoutes()
+ protected function buildRoutes(): void
{
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
// Get the home route
$home = self::resetHomeRoute();
-
// Build routes and taxonomy map.
- /** @var PageInterface $page */
- foreach ($this->instances as $page) {
- if (!$page->root()) {
- // process taxonomy
- $taxonomy->addTaxonomy($page);
+ /** @var PageInterface|string $page */
+ foreach ($this->index as $path => $page) {
+ if (is_string($page)) {
+ $page = $this->get($path);
+ }
+
+ if (!$page || $page->root()) {
+ continue;
+ }
+
+ // process taxonomy
+ $taxonomy->addTaxonomy($page);
+
+ $page_path = $page->path();
+ if (null === $page_path) {
+ throw new RuntimeException('Internal Error');
+ }
- $route = $page->route();
- $raw_route = $page->rawRoute();
- $page_path = $page->path();
+ $route = $page->route();
+ $raw_route = $page->rawRoute();
- // add regular route
+ // add regular route
+ if ($route) {
$this->routes[$route] = $page_path;
+ }
- // add raw route
- if ($raw_route !== $route) {
- $this->routes[$raw_route] = $page_path;
- }
+ // add raw route
+ if ($raw_route && $raw_route !== $route) {
+ $this->routes[$raw_route] = $page_path;
+ }
- // add canonical route
- $route_canonical = $page->routeCanonical();
- if ($route_canonical && ($route !== $route_canonical)) {
- $this->routes[$route_canonical] = $page_path;
- }
+ // add canonical route
+ $route_canonical = $page->routeCanonical();
+ if ($route_canonical && $route !== $route_canonical) {
+ $this->routes[$route_canonical] = $page_path;
+ }
- // add aliases to routes list if they are provided
- $route_aliases = $page->routeAliases();
- if ($route_aliases) {
- foreach ($route_aliases as $alias) {
- $this->routes[$alias] = $page_path;
- }
+ // add aliases to routes list if they are provided
+ $route_aliases = $page->routeAliases();
+ if ($route_aliases) {
+ foreach ($route_aliases as $alias) {
+ $this->routes[$alias] = $page_path;
}
}
}
// Alias and set default route to home page.
- $homeRoute = '/' . $home;
+ $homeRoute = "/{$home}";
if ($home && isset($this->routes[$homeRoute])) {
- $this->routes['/'] = $this->routes[$homeRoute];
- $this->get($this->routes[$homeRoute])->route('/');
+ $home = $this->get($this->routes[$homeRoute]);
+ if ($home) {
+ $this->routes['/'] = $this->routes[$homeRoute];
+ $home->route('/');
+ }
}
}
@@ -1270,15 +2019,14 @@ class Pages
* @param string $order_by
* @param array|null $manual
* @param int|null $sort_flags
- *
- * @throws \RuntimeException
+ * @throws RuntimeException
* @internal
*/
- protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null)
+ protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null): void
{
$list = [];
+ $header_query = null;
$header_default = null;
- $header_query = [];
// do this header query work only once
if (strpos($order_by, 'header.') === 0) {
@@ -1288,9 +2036,9 @@ class Pages
}
foreach ($pages as $key => $info) {
- $child = $this->instances[$key] ?? null;
+ $child = $this->get($key);
if (!$child) {
- throw new \RuntimeException("Page does not exist: {$key}");
+ throw new RuntimeException("Page does not exist: {$key}");
}
switch ($order_by) {
@@ -1325,24 +2073,24 @@ class Pages
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);
- } elseif ($header_value) {
- $list[$key] = $header_value;
- } else {
- $list[$key] = $header_default ?: $key;
+ 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);
+ } elseif ($header_value) {
+ $list[$key] = $header_value;
+ } else {
+ $list[$key] = $header_default ?: $key;
+ }
+ $sort_flags = $sort_flags ?: SORT_REGULAR;
+ break;
}
+ $list[$key] = $key;
$sort_flags = $sort_flags ?: SORT_REGULAR;
- break;
- }
- $list[$key] = $key;
- $sort_flags = $sort_flags ?: SORT_REGULAR;
}
}
@@ -1356,13 +2104,17 @@ class Pages
} else {
// else just sort the list according to specified key
if (extension_loaded('intl') && $this->grav['config']->get('system.intl_enabled')) {
- $locale = setlocale(LC_COLLATE, 0); //`setlocale` with a 0 param returns the current locale set
+ $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set
$col = Collator::create($locale);
if ($col) {
+ $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
- $list = preg_replace_callback('~([0-9]+)\.~', function($number) {
+ $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) {
return sprintf('%032d.', $number[0]);
}, $list);
+ if (!is_array($list)) {
+ throw new RuntimeException('Internal Error');
+ }
$list_vals = array_values($list);
if (is_numeric(array_shift($list_vals))) {
@@ -1389,7 +2141,7 @@ class Pages
foreach ($list as $key => $dummy) {
$info = $pages[$key];
- $order = \array_search($info['slug'], $manual, true);
+ $order = array_search($info['slug'], $manual, true);
if ($order === false) {
$order = $i++;
}
@@ -1399,7 +2151,7 @@ class Pages
$list = $new_list;
// Apply manual ordering to the list.
- asort($list);
+ asort($list, SORT_NUMERIC);
}
foreach ($list as $key => $sort) {
@@ -1412,7 +2164,6 @@ class Pages
* Shuffles an associative array
*
* @param array $list
- *
* @return array
*/
protected function arrayShuffle($list)
@@ -1428,13 +2179,21 @@ class Pages
return $new;
}
+ /**
+ * @return string
+ */
+ protected function getVersion()
+ {
+ return $this->directory ? 'flex' : 'regular';
+ }
+
/**
* Get the Pages cache ID
*
* this is particularly useful to know if pages have changed and you want
* to sync another cache with pages cache - works best in `onPagesInitialized()`
*
- * @return mixed
+ * @return string
*/
public function getPagesCacheId()
{
diff --git a/system/src/Grav/Common/Page/Traits/PageFormTrait.php b/system/src/Grav/Common/Page/Traits/PageFormTrait.php
new file mode 100644
index 00000000..b99e7b75
--- /dev/null
+++ b/system/src/Grav/Common/Page/Traits/PageFormTrait.php
@@ -0,0 +1,126 @@
+ blueprint, ...], where blueprint follows the regular form blueprint format.
+ *
+ * @return array
+ */
+ public function getForms(): array
+ {
+ if (null === $this->_forms) {
+ $header = $this->header();
+
+ // Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
+ $grav = Grav::instance();
+ $grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header]));
+
+ $rules = $header->rules ?? null;
+ if (!is_array($rules)) {
+ $rules = [];
+ }
+
+ $forms = [];
+
+ // First grab page.header.form
+ $form = $this->normalizeForm($header->form ?? null, null, $rules);
+ if ($form) {
+ $forms[$form['name']] = $form;
+ }
+
+ // Append page.header.forms (override singular form if it clashes)
+ $headerForms = $header->forms ?? null;
+ if (is_array($headerForms)) {
+ foreach ($headerForms as $name => $form) {
+ $form = $this->normalizeForm($form, $name, $rules);
+ if ($form) {
+ $forms[$form['name']] = $form;
+ }
+ }
+ }
+
+ $this->_forms = $forms;
+ }
+
+ return $this->_forms;
+ }
+
+ /**
+ * Add forms to this page.
+ *
+ * @param array $new
+ * @param bool $override
+ * @return $this
+ */
+ public function addForms(array $new, $override = true)
+ {
+ // Initialize forms.
+ $this->forms();
+
+ foreach ($new as $name => $form) {
+ $form = $this->normalizeForm($form, $name);
+ $name = $form['name'] ?? null;
+ if ($name && ($override || !isset($this->_forms[$name]))) {
+ $this->_forms[$name] = $form;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Alias of $this->getForms();
+ *
+ * @return array
+ */
+ public function forms(): array
+ {
+ return $this->getForms();
+ }
+
+ /**
+ * @param array|null $form
+ * @param string|null $name
+ * @param array $rules
+ * @return array|null
+ */
+ protected function normalizeForm($form, $name = null, array $rules = []): ?array
+ {
+ if (!is_array($form)) {
+ return null;
+ }
+
+ // Ignore numeric indexes on name.
+ if (!$name || (string)(int)$name === (string)$name) {
+ $name = null;
+ }
+
+ $name = $name ?? $form['name'] ?? $this->slug();
+
+ $formRules = $form['rules'] ?? null;
+ if (!is_array($formRules)) {
+ $formRules = [];
+ }
+
+ return ['name' => $name, 'rules' => $rules + $formRules] + $form;
+ }
+
+ abstract public function header($var = null);
+ abstract public function slug($var = null);
+}
diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php
index 03e3f6eb..c7183680 100644
--- a/system/src/Grav/Common/Page/Types.php
+++ b/system/src/Grav/Common/Page/Types.php
@@ -3,38 +3,52 @@
/**
* @package Grav\Common\Page
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page;
+use Grav\Common\Data\Blueprint;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
+use InvalidArgumentException;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
use RocketTheme\Toolbox\ArrayTraits\Constructor;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\Iterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use function is_string;
+/**
+ * Class Types
+ * @package Grav\Common\Page
+ */
class Types implements \ArrayAccess, \Iterator, \Countable
{
use ArrayAccess, Constructor, Iterator, Countable, Export;
+ /** @var array */
protected $items;
- protected $systemBlueprints;
-
+ /** @var array */
+ protected $systemBlueprints = [];
+
+ /**
+ * @param string $type
+ * @param Blueprint|null $blueprint
+ * @return void
+ */
public function register($type, $blueprint = null)
{
if (!isset($this->items[$type])) {
$this->items[$type] = [];
- } elseif (!$blueprint) {
+ } elseif (null === $blueprint) {
return;
}
- if (!$blueprint && $this->systemBlueprints) {
- $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default'];
+ if (null === $blueprint) {
+ $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default'] ?? null;
}
if ($blueprint) {
@@ -42,19 +56,28 @@ class Types implements \ArrayAccess, \Iterator, \Countable
}
}
- public function scanBlueprints($uri)
+ /**
+ * @return void
+ */
+ public function init()
{
- if (!\is_string($uri)) {
- throw new \InvalidArgumentException('First parameter must be URI');
- }
-
- if (!$this->systemBlueprints) {
+ if (empty($this->systemBlueprints)) {
+ // Register all blueprints from the blueprints stream.
$this->systemBlueprints = $this->findBlueprints('blueprints://pages');
+ foreach ($this->systemBlueprints as $type => $blueprint) {
+ $this->register($type);
+ }
+ }
+ }
- // Register default by default.
- $this->register('default');
-
- $this->register('external');
+ /**
+ * @param string $uri
+ * @return void
+ */
+ public function scanBlueprints($uri)
+ {
+ if (!is_string($uri)) {
+ throw new InvalidArgumentException('First parameter must be URI');
}
foreach ($this->findBlueprints($uri) as $type => $blueprint) {
@@ -62,10 +85,14 @@ class Types implements \ArrayAccess, \Iterator, \Countable
}
}
+ /**
+ * @param string $uri
+ * @return void
+ */
public function scanTemplates($uri)
{
- if (!\is_string($uri)) {
- throw new \InvalidArgumentException('First parameter must be URI');
+ if (!is_string($uri)) {
+ throw new InvalidArgumentException('First parameter must be URI');
}
$options = [
@@ -90,6 +117,9 @@ class Types implements \ArrayAccess, \Iterator, \Countable
}
}
+ /**
+ * @return array
+ */
public function pageSelect()
{
$list = [];
@@ -104,6 +134,9 @@ class Types implements \ArrayAccess, \Iterator, \Countable
return $list;
}
+ /**
+ * @return array
+ */
public function modularSelect()
{
$list = [];
@@ -118,6 +151,10 @@ class Types implements \ArrayAccess, \Iterator, \Countable
return $list;
}
+ /**
+ * @param string $uri
+ * @return array
+ */
private function findBlueprints($uri)
{
$options = [
diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php
index 727fd95e..b32e39e6 100644
--- a/system/src/Grav/Common/Plugin.php
+++ b/system/src/Grav/Common/Plugin.php
@@ -3,44 +3,48 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use ArrayAccess;
+use Composer\Autoload\ClassLoader;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Config\Config;
-use RocketTheme\Toolbox\Event\EventDispatcher;
-use RocketTheme\Toolbox\Event\EventSubscriberInterface;
+use LogicException;
use RocketTheme\Toolbox\File\YamlFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use function defined;
+use function is_bool;
+use function is_string;
-class Plugin implements EventSubscriberInterface, \ArrayAccess
+/**
+ * Class Plugin
+ * @package Grav\Common
+ */
+class Plugin implements EventSubscriberInterface, ArrayAccess
{
- /**
- * @var string
- */
+ /** @var string */
public $name;
-
- /**
- * @var array
- */
+ /** @var array */
public $features = [];
- /**
- * @var Grav
- */
+ /** @var Grav */
protected $grav;
-
- /**
- * @var Config
- */
+ /** @var Config|null */
protected $config;
-
+ /** @var bool */
protected $active = true;
+ /** @var Blueprint|null */
protected $blueprint;
+ /** @var ClassLoader|null */
+ protected $loader;
/**
* By default assign all methods as listeners using the default priority.
@@ -49,7 +53,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public static function getSubscribedEvents()
{
- $methods = get_class_methods(get_called_class());
+ $methods = get_class_methods(static::class);
$list = [];
foreach ($methods as $method) {
@@ -66,17 +70,36 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*
* @param string $name
* @param Grav $grav
- * @param Config $config
+ * @param Config|null $config
*/
public function __construct($name, Grav $grav, Config $config = null)
{
$this->name = $name;
$this->grav = $grav;
+
if ($config) {
$this->setConfig($config);
}
}
+ /**
+ * @return ClassLoader|null
+ * @internal
+ */
+ final public function getAutoloader(): ?ClassLoader
+ {
+ return $this->loader;
+ }
+
+ /**
+ * @param ClassLoader|null $loader
+ * @internal
+ */
+ final public function setAutoloader(?ClassLoader $loader): void
+ {
+ $this->loader = $loader;
+ }
+
/**
* @param Config $config
* @return $this
@@ -95,7 +118,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public function config()
{
- return $this->config["plugins.{$this->name}"];
+ return null !== $this->config ? $this->config["plugins.{$this->name}"] : [];
}
/**
@@ -115,7 +138,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public function isCli()
{
- return \defined('GRAV_CLI');
+ return defined('GRAV_CLI');
}
/**
@@ -126,21 +149,25 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
protected function isPluginActiveAdmin($plugin_route)
{
- $should_run = false;
+ $active = false;
+ /** @var Uri $uri */
$uri = $this->grav['uri'];
+ /** @var Config $config */
+ $config = $this->config ?? $this->grav['config'];
- if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
- $should_run = false;
+ if (strpos($uri->path(), $config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
+ $active = false;
} elseif (isset($uri->paths()[1]) && $uri->paths()[1] === $plugin_route) {
- $should_run = true;
+ $active = true;
}
- return $should_run;
+ return $active;
}
/**
* @param array $events
+ * @return void
*/
protected function enable(array $events)
{
@@ -148,9 +175,9 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
$dispatcher = $this->grav['events'];
foreach ($events as $eventName => $params) {
- if (\is_string($params)) {
+ if (is_string($params)) {
$dispatcher->addListener($eventName, [$this, $params]);
- } elseif (\is_string($params[0])) {
+ } elseif (is_string($params[0])) {
$dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName));
} else {
foreach ($params as $listener) {
@@ -163,22 +190,18 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
/**
* @param array $params
* @param string $eventName
+ * @return int
*/
private function getPriority($params, $eventName)
{
- $grav = Grav::instance();
- $override = implode('.', ["priorities", $this->name, $eventName, $params[0]]);
- if ($grav['config']->get($override) !== null)
- {
- return $grav['config']->get($override);
- } elseif (isset($params[1])) {
- return $params[1];
- }
- return 0;
+ $override = implode('.', ['priorities', $this->name, $eventName, $params[0]]);
+
+ return $this->grav['config']->get($override) ?? $params[1] ?? 0;
}
/**
* @param array $events
+ * @return void
*/
protected function disable(array $events)
{
@@ -186,9 +209,9 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
$dispatcher = $this->grav['events'];
foreach ($events as $eventName => $params) {
- if (\is_string($params)) {
+ if (is_string($params)) {
$dispatcher->removeListener($eventName, [$this, $params]);
- } elseif (\is_string($params[0])) {
+ } elseif (is_string($params[0])) {
$dispatcher->removeListener($eventName, [$this, $params[0]]);
} else {
foreach ($params as $listener) {
@@ -206,12 +229,13 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public function offsetExists($offset)
{
- $this->loadBlueprint();
-
if ($offset === 'title') {
$offset = 'name';
}
- return isset($this->blueprint[$offset]);
+
+ $blueprint = $this->getBlueprint();
+
+ return isset($blueprint[$offset]);
}
/**
@@ -222,12 +246,13 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public function offsetGet($offset)
{
- $this->loadBlueprint();
-
if ($offset === 'title') {
$offset = 'name';
}
- return $this->blueprint[$offset] ?? null;
+
+ $blueprint = $this->getBlueprint();
+
+ return $blueprint[$offset] ?? null;
}
/**
@@ -235,22 +260,35 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*
* @param string $offset The offset to assign the value to.
* @param mixed $value The value to set.
- * @throws \LogicException
+ * @throws LogicException
*/
public function offsetSet($offset, $value)
{
- throw new \LogicException(__CLASS__ . ' blueprints cannot be modified.');
+ throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
}
/**
* Unsets an offset.
*
* @param string $offset The offset to unset.
- * @throws \LogicException
+ * @throws LogicException
*/
public function offsetUnset($offset)
{
- throw new \LogicException(__CLASS__ . ' blueprints cannot be modified.');
+ throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
+ }
+
+ /**
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $array = (array)$this;
+
+ unset($array["\0*\0grav"]);
+ $array["\0*\0config"] = $this->config();
+
+ return $array;
}
/**
@@ -263,40 +301,46 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
* @param string $content The string to perform operations upon
* @param callable $function The anonymous callback function
* @param string $internal_regex Optional internal regex to extra data from
- *
* @return string
*/
protected function parseLinks($content, $function, $internal_regex = '(.*)')
{
- $regex = '/\[plugin:(?:' . $this->name . ')\]\(' . $internal_regex . '\)/i';
+ $regex = '/\[plugin:(?:' . preg_quote($this->name, '/') . ')\]\(' . $internal_regex . '\)/i';
- return preg_replace_callback($regex, $function, $content);
+ $result = preg_replace_callback($regex, $function, $content);
+ \assert($result !== null);
+
+ return $result;
}
/**
* Merge global and page configurations.
*
+ * WARNING: This method modifies page header!
+ *
* @param PageInterface $page The page to merge the configurations with the
* plugin settings.
* @param mixed $deep false = shallow|true = recursive|merge = recursive+unique
* @param array $params Array of additional configuration options to
* merge with the plugin settings.
* @param string $type Is this 'plugins' or 'themes'
- *
* @return Data
*/
protected function mergeConfig(PageInterface $page, $deep = false, $params = [], $type = 'plugins')
{
+ /** @var Config $config */
+ $config = $this->config ?? $this->grav['config'];
+
$class_name = $this->name;
$class_name_merged = $class_name . '.merged';
- $defaults = $this->config->get($type . '.' . $class_name, []);
+ $defaults = $config->get($type . '.' . $class_name, []);
$page_header = $page->header();
$header = [];
if (!isset($page_header->{$class_name_merged}) && isset($page_header->{$class_name})) {
// Get default plugin configurations and retrieve page header configuration
$config = $page_header->{$class_name};
- if (\is_bool($config)) {
+ if (is_bool($config)) {
// Overwrite enabled option with boolean value in page header
$config = ['enabled' => $config];
}
@@ -342,23 +386,26 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
/**
* Persists to disk the plugin parameters currently stored in the Grav Config object
*
- * @param string $plugin_name The name of the plugin whose config it should store.
- *
+ * @param string $name The name of the plugin whose config it should store.
* @return bool
*/
- public static function saveConfig($plugin_name)
+ public static function saveConfig($name)
{
- if (!$plugin_name) {
+ if (!$name) {
return false;
}
$grav = Grav::instance();
+
+ /** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $filename = 'config://plugins/' . $plugin_name . '.yaml';
- $file = YamlFile::instance($locator->findResource($filename, true, true));
- $content = $grav['config']->get('plugins.' . $plugin_name);
+
+ $filename = 'config://plugins/' . $name . '.yaml';
+ $file = YamlFile::instance((string)$locator->findResource($filename, true, true));
+ $content = $grav['config']->get('plugins.' . $name);
$file->save($content);
$file->free();
+ unset($file);
return true;
}
@@ -370,21 +417,28 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
*/
public function getBlueprint()
{
- if (!$this->blueprint) {
+ if (null === $this->blueprint) {
$this->loadBlueprint();
+ \assert($this->blueprint instanceof Blueprint);
}
+
return $this->blueprint;
}
/**
* Load blueprints.
+ *
+ * @return void
*/
protected function loadBlueprint()
{
- if (!$this->blueprint) {
+ if (null === $this->blueprint) {
$grav = Grav::instance();
+ /** @var Plugins $plugins */
$plugins = $grav['plugins'];
- $this->blueprint = $plugins->get($this->name)->blueprints();
+ $data = $plugins->get($this->name);
+ \assert($data !== null);
+ $this->blueprint = $data->blueprints();
}
}
}
diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php
index 358727f3..88b5e524 100644
--- a/system/src/Grav/Common/Plugins.php
+++ b/system/src/Grav/Common/Plugins.php
@@ -3,23 +3,40 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use Exception;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprints;
use Grav\Common\Data\Data;
use Grav\Common\File\CompiledYamlFile;
-use RocketTheme\Toolbox\Event\EventDispatcher;
+use Grav\Events\PluginsLoadedEvent;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use SplFileInfo;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use function get_class;
+use function is_object;
+/**
+ * Class Plugins
+ * @package Grav\Common
+ */
class Plugins extends Iterator
{
+ /** @var array|null */
public $formFieldTypes;
+ /** @var bool */
+ private $plugins_initialized = false;
+
+ /**
+ * Plugins constructor.
+ */
public function __construct()
{
parent::__construct();
@@ -30,17 +47,21 @@ class Plugins extends Iterator
$iterator = $locator->getIterator('plugins://');
$plugins = [];
- foreach($iterator as $directory) {
+ /** @var SplFileInfo $directory */
+ foreach ($iterator as $directory) {
if (!$directory->isDir()) {
continue;
}
$plugins[] = $directory->getFilename();
}
- natsort($plugins);
+ sort($plugins, SORT_NATURAL | SORT_FLAG_CASE);
foreach ($plugins as $plugin) {
- $this->add($this->loadPlugin($plugin));
+ $object = $this->loadPlugin($plugin);
+ if ($object) {
+ $this->add($object);
+ }
}
}
@@ -52,28 +73,36 @@ class Plugins extends Iterator
$blueprints = [];
$formFields = [];
+ $grav = Grav::instance();
+
+ /** @var Config $config */
+ $config = $grav['config'];
+
/** @var Plugin $plugin */
foreach ($this->items as $plugin) {
- if (isset($plugin->features['blueprints'])) {
- $blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints'];
- }
- if (method_exists($plugin, 'getFormFieldTypes')) {
- $formFields[get_class($plugin)] = isset($plugin->features['formfields']) ? $plugin->features['formfields'] : 0;
+ // Setup only enabled plugins.
+ if ($config["plugins.{$plugin->name}.enabled"] && $plugin instanceof Plugin) {
+ if (isset($plugin->features['blueprints'])) {
+ $blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints'];
+ }
+ if (method_exists($plugin, 'getFormFieldTypes')) {
+ $formFields[get_class($plugin)] = $plugin->features['formfields'] ?? 0;
+ }
}
}
if ($blueprints) {
// Order by priority.
- arsort($blueprints);
+ arsort($blueprints, SORT_NUMERIC);
/** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $locator->addPath('blueprints', '', array_keys($blueprints), 'system/blueprints');
+ $locator = $grav['locator'];
+ $locator->addPath('blueprints', '', array_keys($blueprints), ['system', 'blueprints']);
}
if ($formFields) {
// Order by priority.
- arsort($formFields);
+ arsort($formFields, SORT_NUMERIC);
$list = [];
foreach ($formFields as $className => $priority) {
@@ -91,10 +120,14 @@ class Plugins extends Iterator
* Registers all plugins.
*
* @return Plugin[] array of Plugin objects
- * @throws \RuntimeException
+ * @throws RuntimeException
*/
public function init()
{
+ if ($this->plugins_initialized) {
+ return $this->items;
+ }
+
$grav = Grav::instance();
/** @var Config $config */
@@ -106,11 +139,23 @@ class Plugins extends Iterator
foreach ($this->items as $instance) {
// Register only enabled plugins.
if ($config["plugins.{$instance->name}.enabled"] && $instance instanceof Plugin) {
+ // Set plugin configuration.
$instance->setConfig($config);
+ // Register autoloader.
+ if (method_exists($instance, 'autoload')) {
+ $instance->setAutoloader($instance->autoload());
+ }
+ // Register event listeners.
$events->addSubscriber($instance);
}
}
+ // Plugins Loaded Event
+ $event = new PluginsLoadedEvent($grav, $this);
+ $grav->dispatchEvent($event);
+
+ $this->plugins_initialized = true;
+
return $this->items;
}
@@ -118,6 +163,7 @@ class Plugins extends Iterator
* Add a plugin
*
* @param Plugin $plugin
+ * @return void
*/
public function add($plugin)
{
@@ -126,14 +172,55 @@ class Plugins extends Iterator
}
}
+ /**
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $array = (array)$this;
+
+ unset($array["\0Grav\Common\Iterator\0iteratorUnset"]);
+
+ return $array;
+ }
+
+ /**
+ * @return Plugin[] Index of all plugins by plugin name.
+ */
+ public static function getPlugins(): array
+ {
+ /** @var Plugins $plugins */
+ $plugins = Grav::instance()['plugins'];
+
+ $list = [];
+ foreach ($plugins as $instance) {
+ $list[$instance->name] = $instance;
+ }
+
+ return $list;
+ }
+
+ /**
+ * @param string $name Plugin name
+ * @return Plugin|null Plugin object or null if plugin cannot be found.
+ */
+ public static function getPlugin(string $name)
+ {
+ $list = static::getPlugins();
+
+ return $list[$name] ?? null;
+ }
+
/**
* Return list of all plugin data with their blueprints.
*
- * @return array
+ * @return Data[]
*/
public static function all()
{
$grav = Grav::instance();
+
+ /** @var Plugins $plugins */
$plugins = $grav['plugins'];
$list = [];
@@ -142,8 +229,8 @@ class Plugins extends Iterator
try {
$result = self::get($name);
- } catch (\Exception $e) {
- $exception = new \RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e);
+ } catch (Exception $e) {
+ $exception = new RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e);
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
@@ -165,7 +252,6 @@ class Plugins extends Iterator
* Get a plugin by name
*
* @param string $name
- *
* @return Data|null
*/
public static function get($name)
@@ -193,36 +279,52 @@ class Plugins extends Iterator
return $obj;
}
+ /**
+ * @param string $name
+ * @return Plugin|null
+ */
protected function loadPlugin($name)
{
+ // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM!
$grav = Grav::instance();
+ /** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
+ $class = null;
+ // Start by attempting to load the plugin_name.php file.
$file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
-
if (is_file($file)) {
- // Local variables available in the file: $grav, $config, $name, $file
+ // Local variables available in the file: $grav, $name, $file
$class = include_once $file;
+ if (!is_object($class) || !is_subclass_of($class, Plugin::class, true)) {
+ $class = null;
+ }
+ }
+ // If the class hasn't been initialized yet, guess the class name and create a new instance.
+ if (null === $class) {
+ $className = Inflector::camelize($name);
$pluginClassFormat = [
'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
- 'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin'
+ 'Grav\\Plugin\\' . $className . 'Plugin',
+ 'Grav\\Plugin\\' . $className
];
foreach ($pluginClassFormat as $pluginClass) {
- if (class_exists($pluginClass)) {
+ if (is_subclass_of($pluginClass, Plugin::class, true)) {
$class = new $pluginClass($name, $grav);
break;
}
}
- } else {
+ }
+
+ // Log a warning if plugin cannot be found.
+ if (null === $class) {
$grav['log']->addWarning(
- sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name)
+ sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
);
- return null;
}
return $class;
}
-
}
diff --git a/system/src/Grav/Common/Processors/AssetsProcessor.php b/system/src/Grav/Common/Processors/AssetsProcessor.php
index 8b464013..66bb5e3f 100644
--- a/system/src/Grav/Common/Processors/AssetsProcessor.php
+++ b/system/src/Grav/Common/Processors/AssetsProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,12 +13,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class AssetsProcessor
+ * @package Grav\Common\Processors
+ */
class AssetsProcessor extends ProcessorBase
{
+ /** @var string */
public $id = '_assets';
+ /** @var string */
public $title = 'Assets';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$this->container['assets']->init();
diff --git a/system/src/Grav/Common/Processors/BackupsProcessor.php b/system/src/Grav/Common/Processors/BackupsProcessor.php
index d2a822c9..6e960b49 100644
--- a/system/src/Grav/Common/Processors/BackupsProcessor.php
+++ b/system/src/Grav/Common/Processors/BackupsProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,12 +13,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class BackupsProcessor
+ * @package Grav\Common\Processors
+ */
class BackupsProcessor extends ProcessorBase
{
+ /** @var string */
public $id = '_backups';
+ /** @var string */
public $title = 'Backups';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$backups = $this->container['backups'];
diff --git a/system/src/Grav/Common/Processors/ConfigurationProcessor.php b/system/src/Grav/Common/Processors/ConfigurationProcessor.php
deleted file mode 100644
index 83ceb842..00000000
--- a/system/src/Grav/Common/Processors/ConfigurationProcessor.php
+++ /dev/null
@@ -1,30 +0,0 @@
-startTimer();
- $this->container['config']->init();
- $this->container['plugins']->setup();
- $this->stopTimer();
-
- return $handler->handle($request);
- }
-}
diff --git a/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php
index 11a39e89..de7cafcf 100644
--- a/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php
+++ b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php
@@ -3,29 +3,38 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
-use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class DebuggerAssetsProcessor
+ * @package Grav\Common\Processors
+ */
class DebuggerAssetsProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'debugger_assets';
+ /** @var string */
public $title = 'Debugger Assets';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$this->container['debugger']->addAssets();
$this->stopTimer();
return $handler->handle($request);
-
}
}
diff --git a/system/src/Grav/Common/Processors/DebuggerProcessor.php b/system/src/Grav/Common/Processors/DebuggerProcessor.php
deleted file mode 100644
index 892af55c..00000000
--- a/system/src/Grav/Common/Processors/DebuggerProcessor.php
+++ /dev/null
@@ -1,29 +0,0 @@
-startTimer();
- $this->container['debugger']->init();
- $this->stopTimer();
-
- return $handler->handle($request);
- }
-}
diff --git a/system/src/Grav/Common/Processors/ErrorsProcessor.php b/system/src/Grav/Common/Processors/ErrorsProcessor.php
deleted file mode 100644
index b0ef6cae..00000000
--- a/system/src/Grav/Common/Processors/ErrorsProcessor.php
+++ /dev/null
@@ -1,29 +0,0 @@
-startTimer();
- $this->container['errors']->resetHandlers();
- $this->stopTimer();
-
- return $handler->handle($request);
- }
-}
diff --git a/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php
index fd97e9e4..32908bad 100644
--- a/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php
+++ b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -16,6 +16,10 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use RocketTheme\Toolbox\Event\Event;
+/**
+ * Class RequestHandlerEvent
+ * @package Grav\Common\Processors\Events
+ */
class RequestHandlerEvent extends Event
{
/**
diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php
index eca7c544..61144648 100644
--- a/system/src/Grav/Common/Processors/InitializeProcessor.php
+++ b/system/src/Grav/Common/Processors/InitializeProcessor.php
@@ -3,24 +3,44 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Grav\Common\Config\Config;
+use Grav\Common\Debugger;
+use Grav\Common\Errors\Errors;
use Grav\Common\Grav;
+use Grav\Common\Page\Pages;
+use Grav\Common\Plugins;
+use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\Utils;
+use Grav\Framework\File\Formatter\YamlFormatter;
+use Grav\Framework\File\YamlFile;
+use Grav\Framework\Psr7\Response;
use Grav\Framework\Session\Exceptions\SessionException;
+use Monolog\Formatter\LineFormatter;
+use Monolog\Handler\SyslogHandler;
+use Monolog\Logger;
+use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use function defined;
+use function in_array;
+/**
+ * Class InitializeProcessor
+ * @package Grav\Common\Processors
+ */
class InitializeProcessor extends ProcessorBase
{
- public $id = 'init';
+ /** @var string */
+ public $id = '_init';
+ /** @var string */
public $title = 'Initialize';
/** @var bool */
@@ -28,6 +48,7 @@ class InitializeProcessor extends ProcessorBase
/**
* @param Grav $grav
+ * @return void
*/
public static function initializeCli(Grav $grav)
{
@@ -39,13 +60,283 @@ class InitializeProcessor extends ProcessorBase
}
}
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $this->startTimer('_init', 'Initialize');
+
+ // Load configuration.
+ $config = $this->initializeConfig();
+
+ // Initialize logger.
+ $this->initializeLogger($config);
+
+ // Initialize error handlers.
+ $this->initializeErrors();
+
+ // Initialize debugger.
+ $debugger = $this->initializeDebugger();
+
+ // Debugger can return response right away.
+ $response = $this->handleDebuggerRequest($debugger, $request);
+ if ($response) {
+ $this->stopTimer('_init');
+
+ return $response;
+ }
+
+ // Initialize output buffering.
+ $this->initializeOutputBuffering($config);
+
+ // Set timezone, locale.
+ $this->initializeLocale($config);
+
+ // Load plugins.
+ $this->initializePlugins();
+
+ // Load pages.
+ $this->initializePages($config);
+
+ // Load accounts (decides class to be used).
+ // TODO: remove in 2.0.
+ $this->container['accounts'];
+
+ // Initialize URI (uses session, see issue #3269).
+ $this->initializeUri($config);
+
+ // Initialize session.
+ $this->initializeSession($config);
+
+ // Grav may return redirect response right away.
+ $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
+ if ($redirectCode) {
+ $response = $this->handleRedirectRequest($request, $redirectCode > 300 ? $redirectCode : null);
+ if ($response) {
+ $this->stopTimer('_init');
+
+ return $response;
+ }
+ }
+
+ $this->stopTimer('_init');
+
+ // Wrap call to next handler so that debugger can profile it.
+ /** @var Response $response */
+ $response = $debugger->profile(static function () use ($handler, $request) {
+ return $handler->handle($request);
+ });
+
+ // Log both request and response and return the response.
+ return $debugger->logRequest($request, $response);
+ }
+
+ public function processCli(): void
+ {
+ // Load configuration.
+ $config = $this->initializeConfig();
+
+ // Initialize logger.
+ $this->initializeLogger($config);
+
+ // Disable debugger.
+ $this->container['debugger']->enabled(false);
+
+ // Set timezone, locale.
+ $this->initializeLocale($config);
+
+ // Load plugins.
+ $this->initializePlugins();
+
+ // Load pages.
+ $this->initializePages($config);
+
+ // Initialize URI.
+ $this->initializeUri($config);
+
+ // Load accounts (decides class to be used).
+ // TODO: remove in 2.0.
+ $this->container['accounts'];
+ }
+
+ /**
+ * @return Config
+ */
+ protected function initializeConfig(): Config
{
- $this->startTimer();
+ $this->startTimer('_init_config', 'Configuration');
+
+ // Initialize Configuration
+ $grav = $this->container;
/** @var Config $config */
- $config = $this->container['config'];
- $config->debug();
+ $config = $grav['config'];
+ $config->init();
+ $grav['plugins']->setup();
+
+ if (defined('GRAV_SCHEMA') && $config->get('versions') === null) {
+ $filename = USER_DIR . 'config/versions.yaml';
+ if (!is_file($filename)) {
+ $versions = [
+ 'core' => [
+ 'grav' => [
+ 'version' => GRAV_VERSION,
+ 'schema' => GRAV_SCHEMA
+ ]
+ ]
+ ];
+ $config->set('versions', $versions);
+
+ $file = new YamlFile($filename, new YamlFormatter(['inline' => 4]));
+ $file->save($versions);
+ }
+ }
+
+ // Override configuration using the environment.
+ $prefix = 'GRAV_CONFIG';
+ $env = getenv($prefix);
+ if ($env) {
+ $cPrefix = $prefix . '__';
+ $aPrefix = $prefix . '_ALIAS__';
+ $cLen = strlen($cPrefix);
+ $aLen = strlen($aPrefix);
+
+ $keys = $aliases = [];
+ $env = $_ENV + $_SERVER;
+ foreach ($env as $key => $value) {
+ if (!str_starts_with($key, $prefix)) {
+ continue;
+ }
+ if (str_starts_with($key, $cPrefix)) {
+ $key = str_replace('__', '.', substr($key, $cLen));
+ $keys[$key] = $value;
+ } elseif (str_starts_with($key, $aPrefix)) {
+ $key = substr($key, $aLen);
+ $aliases[$key] = $value;
+ }
+ }
+ $list = [];
+ foreach ($keys as $key => $value) {
+ foreach ($aliases as $alias => $real) {
+ $key = str_replace($alias, $real, $key);
+ }
+ $list[$key] = $value;
+ $config->set($key, $value);
+ }
+ }
+
+ $this->stopTimer('_init_config');
+
+ return $config;
+ }
+
+ /**
+ * @param Config $config
+ * @return Logger
+ */
+ protected function initializeLogger(Config $config): Logger
+ {
+ $this->startTimer('_init_logger', 'Logger');
+
+ $grav = $this->container;
+
+ // Initialize Logging
+ /** @var Logger $log */
+ $log = $grav['log'];
+
+ if ($config->get('system.log.handler', 'file') === 'syslog') {
+ $log->popHandler();
+
+ $facility = $config->get('system.log.syslog.facility', 'local6');
+ $logHandler = new SyslogHandler('grav', $facility);
+ $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
+ $logHandler->setFormatter($formatter);
+
+ $log->pushHandler($logHandler);
+ }
+
+ $this->stopTimer('_init_logger');
+
+ return $log;
+ }
+
+ /**
+ * @return Errors
+ */
+ protected function initializeErrors(): Errors
+ {
+ $this->startTimer('_init_errors', 'Error Handlers Reset');
+
+ $grav = $this->container;
+
+ // Initialize Error Handlers
+ /** @var Errors $errors */
+ $errors = $grav['errors'];
+ $errors->resetHandlers();
+
+ $this->stopTimer('_init_errors');
+
+ return $errors;
+ }
+
+ /**
+ * @return Debugger
+ */
+ protected function initializeDebugger(): Debugger
+ {
+ $this->startTimer('_init_debugger', 'Init Debugger');
+
+ $grav = $this->container;
+
+ /** @var Debugger $debugger */
+ $debugger = $grav['debugger'];
+ $debugger->init();
+
+ $this->stopTimer('_init_debugger');
+
+ return $debugger;
+ }
+
+ /**
+ * @param Debugger $debugger
+ * @param ServerRequestInterface $request
+ * @return ResponseInterface|null
+ */
+ protected function handleDebuggerRequest(Debugger $debugger, ServerRequestInterface $request): ?ResponseInterface
+ {
+ // Clockwork integration.
+ $clockwork = $debugger->getClockwork();
+ if ($clockwork) {
+ $server = $request->getServerParams();
+// $baseUri = str_replace('\\', '/', dirname(parse_url($server['SCRIPT_NAME'], PHP_URL_PATH)));
+// if ($baseUri === '/') {
+// $baseUri = '';
+// }
+ $requestTime = $server['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
+
+ $request = $request->withAttribute('request_time', $requestTime);
+
+ // Handle clockwork API calls.
+ $uri = $request->getUri();
+ if (Utils::contains($uri->getPath(), '/__clockwork/')) {
+ return $debugger->debuggerRequest($request);
+ }
+
+ $this->container['clockwork'] = $clockwork;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param Config $config
+ */
+ protected function initializeOutputBuffering(Config $config): void
+ {
+ $this->startTimer('_init_ob', 'Initialize Output Buffering');
// Use output buffering to prevent headers from being sent too early.
ob_start();
@@ -54,75 +345,116 @@ class InitializeProcessor extends ProcessorBase
ob_start();
}
+ $this->stopTimer('_init_ob');
+ }
+
+ /**
+ * @param Config $config
+ */
+ protected function initializeLocale(Config $config): void
+ {
+ $this->startTimer('_init_locale', 'Initialize Locale');
+
// Initialize the timezone.
$timezone = $config->get('system.timezone');
if ($timezone) {
date_default_timezone_set($timezone);
}
- // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
- if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
- // TODO: remove in 2.0.
- $this->container['accounts'];
+ $grav = $this->container;
+ $grav->setLocale();
- try {
- $this->container['session']->init();
- } catch (SessionException $e) {
- $this->container['session']->init();
- $message = 'Session corruption detected, restarting session...';
- $this->addMessage($message);
- $this->container['messages']->add($message, 'error');
- }
- }
+ $this->stopTimer('_init_locale');
+ }
- /** @var Uri $uri */
- $uri = $this->container['uri'];
- $uri->init();
+ protected function initializePlugins(): Plugins
+ {
+ $this->startTimer('_init_plugins_load', 'Load Plugins');
- // Redirect pages with trailing slash if configured to do so.
- $path = $uri->path() ?: '/';
- if ($path !== '/'
- && $config->get('system.pages.redirect_trailing_slash', false)
- && Utils::endsWith($path, '/')) {
+ $grav = $this->container;
- $redirect = (string) $uri::getCurrentRoute()->toString();
- $this->container->redirect($redirect);
- }
+ /** @var Plugins $plugins */
+ $plugins = $grav['plugins'];
+ $plugins->init();
- $this->container->setLocale();
- $this->stopTimer();
+ $this->stopTimer('_init_plugins_load');
- return $handler->handle($request);
+ return $plugins;
}
- public function processCli(): void
+ protected function initializePages(Config $config): Pages
{
- // Load configuration.
- $this->container['config']->init();
- $this->container['plugins']->setup();
+ $this->startTimer('_init_pages_register', 'Load Pages');
- // Disable debugger.
- $this->container['debugger']->enabled(false);
+ $grav = $this->container;
- // Set timezone, locale.
- /** @var Config $config */
- $config = $this->container['config'];
- $timezone = $config->get('system.timezone');
- if ($timezone) {
- date_default_timezone_set($timezone);
+ /** @var Pages $pages */
+ $pages = $grav['pages'];
+ // Upgrading from older Grav versions won't work without checking if the method exists.
+ if (method_exists($pages, 'register')) {
+ $pages->register();
}
- $this->container->setLocale();
- // Load plugins.
- $this->container['plugins']->init();
+ $this->stopTimer('_init_pages_register');
+
+ return $pages;
+ }
+
+
+ protected function initializeUri(Config $config): void
+ {
+ $this->startTimer('_init_uri', 'Initialize URI');
+
+ $grav = $this->container;
- // Initialize URI.
/** @var Uri $uri */
- $uri = $this->container['uri'];
+ $uri = $grav['uri'];
$uri->init();
- // Load accounts.
- // TODO: remove in 2.0.
- $this->container['accounts'];
+ $this->stopTimer('_init_uri');
+ }
+
+ protected function handleRedirectRequest(RequestInterface $request, int $code = null): ?ResponseInterface
+ {
+ if (!in_array($request->getMethod(), ['GET', 'HEAD'])) {
+ return null;
+ }
+
+ // Redirect pages with trailing slash if configured to do so.
+ $uri = $request->getUri();
+ $path = $uri->getPath() ?: '/';
+ $root = $this->container['uri']->rootUrl();
+
+ if ($path !== $root && $path !== $root . '/' && Utils::endsWith($path, '/')) {
+ // Use permanent redirect for SEO reasons.
+ return $this->container->getRedirectResponse((string)$uri->withPath(rtrim($path, '/')), $code);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param Config $config
+ */
+ protected function initializeSession(Config $config): void
+ {
+ // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
+ if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
+ $this->startTimer('_init_session', 'Start Session');
+
+ /** @var Session $session */
+ $session = $this->container['session'];
+
+ try {
+ $session->init();
+ } catch (SessionException $e) {
+ $session->init();
+ $message = 'Session corruption detected, restarting session...';
+ $this->addMessage($message);
+ $this->container['messages']->add($message, 'error');
+ }
+
+ $this->stopTimer('_init_session');
+ }
}
}
diff --git a/system/src/Grav/Common/Processors/LoggerProcessor.php b/system/src/Grav/Common/Processors/LoggerProcessor.php
deleted file mode 100644
index af3c1699..00000000
--- a/system/src/Grav/Common/Processors/LoggerProcessor.php
+++ /dev/null
@@ -1,50 +0,0 @@
-startTimer();
-
- $grav = $this->container;
-
- /** @var Config $config */
- $config = $grav['config'];
-
- switch ($config->get('system.log.handler', 'file')) {
- case 'syslog':
- $log = $grav['log'];
- $log->popHandler();
-
- $facility = $config->get('system.log.syslog.facility', 'local6');
- $logHandler = new SyslogHandler('grav', $facility);
- $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
- $logHandler->setFormatter($formatter);
-
- $log->pushHandler($logHandler);
- break;
- }
- $this->stopTimer();
-
- return $handler->handle($request);
- }
-}
diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php
index ede22e5c..470ca907 100644
--- a/system/src/Grav/Common/Processors/PagesProcessor.php
+++ b/system/src/Grav/Common/Processors/PagesProcessor.php
@@ -3,24 +3,38 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Form\Forms;
use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use RuntimeException;
+/**
+ * Class PagesProcessor
+ * @package Grav\Common\Processors
+ */
class PagesProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'pages';
+ /** @var string */
public $title = 'Pages';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
@@ -35,16 +49,25 @@ class PagesProcessor extends ProcessorBase
$page = $this->container['page'];
if (!$page->routable()) {
+ $exception = new RequestException($request, 'Page Not Found', 404);
+ $route = $this->container['route'];
// If no page found, fire event
- $event = new Event(['page' => $page]);
+ $event = new Event([
+ 'page' => $page,
+ 'code' => $exception->getCode(),
+ 'message' => $exception->getMessage(),
+ 'exception' => $exception,
+ 'route' => $route,
+ 'request' => $request
+ ]);
$event->page = null;
$event = $this->container->fireEvent('onPageNotFound', $event);
if (isset($event->page)) {
- unset ($this->container['page']);
+ unset($this->container['page']);
$this->container['page'] = $page = $event->page;
} else {
- throw new \RuntimeException('Page Not Found', 404);
+ throw new RuntimeException('Page Not Found', 404);
}
$this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()}) [Not Found fallback]");
@@ -53,12 +76,18 @@ class PagesProcessor extends ProcessorBase
$task = $this->container['task'];
$action = $this->container['action'];
+
+ /** @var Forms $forms */
+ $forms = $this->container['forms'] ?? null;
+ $form = $forms ? $forms->getActiveForm() : null;
+
+ $options = ['page' => $page, 'form' => $form, 'request' => $request];
if ($task) {
- $event = new Event(['task' => $task, 'page' => $page]);
+ $event = new Event(['task' => $task] + $options);
$this->container->fireEvent('onPageTask', $event);
$this->container->fireEvent('onPageTask.' . $task, $event);
} elseif ($action) {
- $event = new Event(['action' => $action, 'page' => $page]);
+ $event = new Event(['action' => $action] + $options);
$this->container->fireEvent('onPageAction', $event);
$this->container->fireEvent('onPageAction.' . $action, $event);
}
diff --git a/system/src/Grav/Common/Processors/PluginsProcessor.php b/system/src/Grav/Common/Processors/PluginsProcessor.php
index 9d2943b6..485578e4 100644
--- a/system/src/Grav/Common/Processors/PluginsProcessor.php
+++ b/system/src/Grav/Common/Processors/PluginsProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,18 +13,27 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class PluginsProcessor
+ * @package Grav\Common\Processors
+ */
class PluginsProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'plugins';
- public $title = 'Plugins';
+ /** @var string */
+ public $title = 'Initialize Plugins';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
- // TODO: remove in 2.0.
- $this->container['accounts'];
- $this->container['plugins']->init();
- $this->container->fireEvent('onPluginsInitialized');
+ $grav = $this->container;
+ $grav->fireEvent('onPluginsInitialized');
$this->stopTimer();
return $handler->handle($request);
diff --git a/system/src/Grav/Common/Processors/ProcessorBase.php b/system/src/Grav/Common/Processors/ProcessorBase.php
index 5ce7bfd2..a3506f59 100644
--- a/system/src/Grav/Common/Processors/ProcessorBase.php
+++ b/system/src/Grav/Common/Processors/ProcessorBase.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,34 +12,56 @@ namespace Grav\Common\Processors;
use Grav\Common\Debugger;
use Grav\Common\Grav;
+/**
+ * Class ProcessorBase
+ * @package Grav\Common\Processors
+ */
abstract class ProcessorBase implements ProcessorInterface
{
/** @var Grav */
protected $container;
+ /** @var string */
public $id = 'processorbase';
+ /** @var string */
public $title = 'ProcessorBase';
+ /**
+ * ProcessorBase constructor.
+ * @param Grav $container
+ */
public function __construct(Grav $container)
{
$this->container = $container;
}
- protected function startTimer($id = null, $title = null)
+ /**
+ * @param string|null $id
+ * @param string|null $title
+ */
+ protected function startTimer($id = null, $title = null): void
{
/** @var Debugger $debugger */
$debugger = $this->container['debugger'];
$debugger->startTimer($id ?? $this->id, $title ?? $this->title);
}
- protected function stopTimer($id = null)
+ /**
+ * @param string|null $id
+ */
+ protected function stopTimer($id = null): void
{
/** @var Debugger $debugger */
$debugger = $this->container['debugger'];
$debugger->stopTimer($id ?? $this->id);
}
- protected function addMessage($message, $label = 'info', $isString = true)
+ /**
+ * @param string $message
+ * @param string $label
+ * @param bool $isString
+ */
+ protected function addMessage($message, $label = 'info', $isString = true): void
{
/** @var Debugger $debugger */
$debugger = $this->container['debugger'];
diff --git a/system/src/Grav/Common/Processors/ProcessorInterface.php b/system/src/Grav/Common/Processors/ProcessorInterface.php
index a8e4aaae..8a6edbe1 100644
--- a/system/src/Grav/Common/Processors/ProcessorInterface.php
+++ b/system/src/Grav/Common/Processors/ProcessorInterface.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -11,6 +11,10 @@ namespace Grav\Common\Processors;
use Psr\Http\Server\MiddlewareInterface;
+/**
+ * Interface ProcessorInterface
+ * @package Grav\Common\Processors
+ */
interface ProcessorInterface extends MiddlewareInterface
{
}
diff --git a/system/src/Grav/Common/Processors/RenderProcessor.php b/system/src/Grav/Common/Processors/RenderProcessor.php
index 06a6c79f..329da22a 100644
--- a/system/src/Grav/Common/Processors/RenderProcessor.php
+++ b/system/src/Grav/Common/Processors/RenderProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,13 +14,25 @@ use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use RocketTheme\Toolbox\Event\Event;
+/**
+ * Class RenderProcessor
+ * @package Grav\Common\Processors
+ */
class RenderProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'render';
+ /** @var string */
public $title = 'Render';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
@@ -31,23 +43,27 @@ class RenderProcessor extends ProcessorBase
return $output;
}
- ob_start();
+ /** @var PageInterface $page */
+ $page = $this->container['page'];
// Use internal Grav output.
$container->output = $output;
- $container->fireEvent('onOutputGenerated');
+
+ ob_start();
+
+ $event = new Event(['page' => $page, 'output' => &$container->output]);
+ $container->fireEvent('onOutputGenerated', $event);
echo $container->output;
+ $html = ob_get_clean();
+
// remove any output
$container->output = '';
- $this->container->fireEvent('onOutputRendered');
+ $event = new Event(['page' => $page, 'output' => $html]);
+ $this->container->fireEvent('onOutputRendered', $event);
- $html = ob_get_clean();
-
- /** @var PageInterface $page */
- $page = $this->container['page'];
$this->stopTimer();
return new Response($page->httpResponseCode(), $page->httpHeaders(), $html);
diff --git a/system/src/Grav/Common/Processors/RequestProcessor.php b/system/src/Grav/Common/Processors/RequestProcessor.php
index 97564e69..971fb674 100644
--- a/system/src/Grav/Common/Processors/RequestProcessor.php
+++ b/system/src/Grav/Common/Processors/RequestProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,12 +15,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class RequestProcessor
+ * @package Grav\Common\Processors
+ */
class RequestProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'request';
+ /** @var string */
public $title = 'Request';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
diff --git a/system/src/Grav/Common/Processors/SchedulerProcessor.php b/system/src/Grav/Common/Processors/SchedulerProcessor.php
index 64f3fba8..69cc1638 100644
--- a/system/src/Grav/Common/Processors/SchedulerProcessor.php
+++ b/system/src/Grav/Common/Processors/SchedulerProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,12 +14,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class SchedulerProcessor
+ * @package Grav\Common\Processors
+ */
class SchedulerProcessor extends ProcessorBase
{
+ /** @var string */
public $id = '_scheduler';
+ /** @var string */
public $title = 'Scheduler';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$scheduler = $this->container['scheduler'];
diff --git a/system/src/Grav/Common/Processors/TasksProcessor.php b/system/src/Grav/Common/Processors/TasksProcessor.php
index aaf5cdf3..07e0934a 100644
--- a/system/src/Grav/Common/Processors/TasksProcessor.php
+++ b/system/src/Grav/Common/Processors/TasksProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,12 +14,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class TasksProcessor
+ * @package Grav\Common\Processors
+ */
class TasksProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'tasks';
+ /** @var string */
public $title = 'Tasks';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
@@ -42,7 +53,6 @@ class TasksProcessor extends ProcessorBase
$this->stopTimer();
return $response;
-
} catch (NotFoundException $e) {
// Task not found: Let it pass through.
}
diff --git a/system/src/Grav/Common/Processors/ThemesProcessor.php b/system/src/Grav/Common/Processors/ThemesProcessor.php
index d1c6ae56..951dc79e 100644
--- a/system/src/Grav/Common/Processors/ThemesProcessor.php
+++ b/system/src/Grav/Common/Processors/ThemesProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,12 +13,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class ThemesProcessor
+ * @package Grav\Common\Processors
+ */
class ThemesProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'themes';
+ /** @var string */
public $title = 'Themes';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$this->container['themes']->init();
diff --git a/system/src/Grav/Common/Processors/TwigProcessor.php b/system/src/Grav/Common/Processors/TwigProcessor.php
index 4ed247d0..6604b5c0 100644
--- a/system/src/Grav/Common/Processors/TwigProcessor.php
+++ b/system/src/Grav/Common/Processors/TwigProcessor.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Processors
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,12 +13,23 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+/**
+ * Class TwigProcessor
+ * @package Grav\Common\Processors
+ */
class TwigProcessor extends ProcessorBase
{
+ /** @var string */
public $id = 'twig';
+ /** @var string */
public $title = 'Twig';
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$this->container['twig']->init();
diff --git a/system/src/Grav/Common/Scheduler/Cron.php b/system/src/Grav/Common/Scheduler/Cron.php
index 3a42b29e..5127a997 100644
--- a/system/src/Grav/Common/Scheduler/Cron.php
+++ b/system/src/Grav/Common/Scheduler/Cron.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Scheduler
* @author Originally based on jqCron by Arnaud Buathier modified for Grav integration
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -45,6 +45,15 @@ namespace Grav\Common\Scheduler;
* var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5));
* // bool(true)
*/
+
+use DateInterval;
+use DateTime;
+use RuntimeException;
+use function count;
+use function in_array;
+use function is_array;
+use function is_string;
+
class Cron
{
public const TYPE_UNDEFINED = '';
@@ -69,7 +78,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 +95,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',
@@ -127,7 +136,6 @@ class Cron
protected $dom = [];
/**
- *
* @param string|null $cron
*/
public function __construct($cron = null)
@@ -138,7 +146,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCron()
@@ -153,7 +160,6 @@ class Cron
}
/**
- *
* @param string $lang 'fr' or 'en'
* @return string
*/
@@ -191,7 +197,7 @@ class Cron
}
// month + year
- if (\in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) {
+ if (in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) {
$elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth());
}
@@ -205,7 +211,7 @@ class Cron
}
// day + week + month + year
- if (\in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) {
+ if (in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) {
$elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes());
}
@@ -213,7 +219,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getType()
@@ -250,9 +255,8 @@ class Cron
}
/**
- *
* @param string $cron
- * @return Cron
+ * @return $this
*/
public function setCron($cron)
{
@@ -261,8 +265,8 @@ class Cron
$cron = preg_replace('/\s+/', ' ', $cron);
// explode
$elements = explode(' ', $cron);
- if (\count($elements) !== 5) {
- throw new \RuntimeException('Bad number of elements');
+ if (count($elements) !== 5) {
+ throw new RuntimeException('Bad number of elements');
}
$this->cron = $cron;
@@ -276,7 +280,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCronMinutes()
@@ -285,7 +288,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCronHours()
@@ -294,7 +296,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCronDaysOfMonth()
@@ -303,7 +304,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCronMonths()
@@ -312,7 +312,6 @@ class Cron
}
/**
- *
* @return string
*/
public function getCronDaysOfWeek()
@@ -321,7 +320,6 @@ class Cron
}
/**
- *
* @return array
*/
public function getMinutes()
@@ -330,7 +328,6 @@ class Cron
}
/**
- *
* @return array
*/
public function getHours()
@@ -339,7 +336,6 @@ class Cron
}
/**
- *
* @return array
*/
public function getDaysOfMonth()
@@ -348,7 +344,6 @@ class Cron
}
/**
- *
* @return array
*/
public function getMonths()
@@ -357,7 +352,6 @@ class Cron
}
/**
- *
* @return array
*/
public function getDaysOfWeek()
@@ -366,9 +360,8 @@ class Cron
}
/**
- *
- * @param string|array $minutes
- * @return Cron
+ * @param string|string[] $minutes
+ * @return $this
*/
public function setMinutes($minutes)
{
@@ -378,9 +371,8 @@ class Cron
}
/**
- *
- * @param string|array $hours
- * @return Cron
+ * @param string|string[] $hours
+ * @return $this
*/
public function setHours($hours)
{
@@ -390,9 +382,8 @@ class Cron
}
/**
- *
- * @param string|array $months
- * @return Cron
+ * @param string|string[] $months
+ * @return $this
*/
public function setMonths($months)
{
@@ -402,9 +393,8 @@ class Cron
}
/**
- *
- * @param string|array $dow
- * @return Cron
+ * @param string|string[] $dow
+ * @return $this
*/
public function setDaysOfWeek($dow)
{
@@ -414,9 +404,8 @@ class Cron
}
/**
- *
- * @param string|array $dom
- * @return Cron
+ * @param string|string[] $dom
+ * @return $this
*/
public function setDaysOfMonth($dom)
{
@@ -426,73 +415,68 @@ class Cron
}
/**
- *
* @param mixed $date
* @param int $min
* @param int $hour
* @param int $day
* @param int $month
* @param int $weekday
- * @return \DateTime
+ * @return DateTime
*/
protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday)
{
if (is_numeric($date) && (int)$date == $date) {
- $date = new \DateTime('@' . $date);
- }
- elseif (is_string($date)) {
- $date = new \DateTime('@' . strtotime($date));
+ $date = new DateTime('@' . $date);
+ } elseif (is_string($date)) {
+ $date = new DateTime('@' . strtotime($date));
}
- if ($date instanceof \DateTime) {
+ if ($date instanceof DateTime) {
$min = (int)$date->format('i');
$hour = (int)$date->format('H');
$day = (int)$date->format('d');
$month = (int)$date->format('m');
$weekday = (int)$date->format('w'); // 0-6
- }
- else {
- throw new \RuntimeException('Date format not supported');
+ } else {
+ throw new RuntimeException('Date format not supported');
}
- return new \DateTime($date->format('Y-m-d H:i:sP'));
+ return new DateTime($date->format('Y-m-d H:i:sP'));
}
/**
- *
- * @param int|string|\DateTime $date
+ * @param int|string|DateTime $date
*/
public function matchExact($date)
{
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
return
- (empty($this->minutes) || \in_array($min, $this->minutes, true)) &&
- (empty($this->hours) || \in_array($hour, $this->hours, true)) &&
- (empty($this->dom) || \in_array($day, $this->dom, true)) &&
- (empty($this->months) || \in_array($month, $this->months, true)) &&
- (empty($this->dow) || \in_array($weekday, $this->dow, true) || ($weekday == 0 && \in_array(7, $this->dow, true)) || ($weekday == 7 && \in_array(0, $this->dow, true))
+ (empty($this->minutes) || in_array($min, $this->minutes, true)) &&
+ (empty($this->hours) || in_array($hour, $this->hours, true)) &&
+ (empty($this->dom) || in_array($day, $this->dom, true)) &&
+ (empty($this->months) || in_array($month, $this->months, true)) &&
+ (empty($this->dow) || in_array($weekday, $this->dow, true) || ($weekday == 0 && in_array(7, $this->dow, true)) || ($weekday == 7 && in_array(0, $this->dow, true))
);
}
/**
- *
- * @param int|string|\DateTime $date
+ * @param int|string|DateTime $date
* @param int $minuteBefore
* @param int $minuteAfter
*/
public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0)
{
if ($minuteBefore > 0) {
- throw new \RuntimeException('MinuteBefore parameter cannot be positive !');
+ throw new RuntimeException('MinuteBefore parameter cannot be positive !');
}
if ($minuteAfter < 0) {
- throw new \RuntimeException('MinuteAfter parameter cannot be negative !');
+ throw new RuntimeException('MinuteAfter parameter cannot be negative !');
}
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
- $interval = new \DateInterval('PT1M'); // 1 min
+ $interval = new DateInterval('PT1M'); // 1 min
if ($minuteBefore !== 0) {
- $date->sub(new \DateInterval('PT' . abs($minuteBefore) . 'M'));
+ $date->sub(new DateInterval('PT' . abs($minuteBefore) . 'M'));
}
$n = $minuteAfter - $minuteBefore + 1;
for ($i = 0; $i < $n; $i++) {
@@ -506,14 +490,13 @@ class Cron
}
/**
- *
* @param array $array
* @return string
*/
protected function arrayToCron($array)
{
- $n = \count($array);
- if (!\is_array($array) || $n === 0) {
+ $n = count($array);
+ if (!is_array($array) || $n === 0) {
return '*';
}
@@ -522,9 +505,8 @@ class Cron
for ($i = 1; $i < $n; $i++) {
if ($array[$i] == $c + 1) {
$c = $array[$i];
- $cron[\count($cron) - 1] = $s . '-' . $c;
- }
- else {
+ $cron[count($cron) - 1] = $s . '-' . $c;
+ } else {
$s = $c = $array[$i];
$cron[] = $c;
}
@@ -543,7 +525,7 @@ class Cron
protected function cronToArray($string, $min, $max)
{
$array = [];
- if (\is_array($string)) {
+ if (is_array($string)) {
foreach ($string as $val) {
if (is_numeric($val) && (int)$val == $val && $val >= $min && $val <= $max) {
$array[] = (int)$val;
@@ -588,7 +570,7 @@ class Cron
return [];
}
}
- sort($array);
+ sort($array, SORT_NUMERIC);
return $array;
}
diff --git a/system/src/Grav/Common/Scheduler/IntervalTrait.php b/system/src/Grav/Common/Scheduler/IntervalTrait.php
index b382c9d7..cc5c1654 100644
--- a/system/src/Grav/Common/Scheduler/IntervalTrait.php
+++ b/system/src/Grav/Common/Scheduler/IntervalTrait.php
@@ -3,14 +3,20 @@
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
use Cron\CronExpression;
+use InvalidArgumentException;
+use function is_string;
+/**
+ * Trait IntervalTrait
+ * @package Grav\Common\Scheduler
+ */
trait IntervalTrait
{
/**
@@ -59,7 +65,7 @@ trait IntervalTrait
*/
public function daily($hour = 0, $minute = 0)
{
- if (\is_string($hour)) {
+ if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
@@ -79,7 +85,7 @@ trait IntervalTrait
*/
public function weekly($weekday = 0, $hour = 0, $minute = 0)
{
- if (\is_string($hour)) {
+ if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
@@ -100,7 +106,7 @@ trait IntervalTrait
*/
public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0)
{
- if (\is_string($hour)) {
+ if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
@@ -353,11 +359,11 @@ trait IntervalTrait
/**
* Validate sequence of cron expression.
*
- * @param int|string $minute
- * @param int|string $hour
- * @param int|string $day
- * @param int|string $month
- * @param int|string $weekday
+ * @param int|string|null $minute
+ * @param int|string|null $hour
+ * @param int|string|null $day
+ * @param int|string|null $month
+ * @param int|string|null $weekday
* @return array
*/
private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null)
@@ -374,7 +380,7 @@ trait IntervalTrait
/**
* Validate sequence of cron expression.
*
- * @param int|string $value
+ * @param int|string|null $value
* @param int $min
* @param int $max
* @return mixed
@@ -388,7 +394,7 @@ trait IntervalTrait
if (! is_numeric($value) ||
! ($value >= $min && $value <= $max)
) {
- throw new \InvalidArgumentException(
+ throw new InvalidArgumentException(
"Invalid value: it should be '*' or between {$min} and {$max}."
);
}
@@ -396,4 +402,3 @@ trait IntervalTrait
return $value;
}
}
-
diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php
index 6250f1d6..13059a57 100644
--- a/system/src/Grav/Common/Scheduler/Job.php
+++ b/system/src/Grav/Common/Scheduler/Job.php
@@ -3,42 +3,79 @@
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
+use Closure;
use Cron\CronExpression;
+use DateTime;
use Grav\Common\Grav;
+use InvalidArgumentException;
+use RuntimeException;
use Symfony\Component\Process\Process;
+use function call_user_func;
+use function call_user_func_array;
+use function count;
+use function is_array;
+use function is_callable;
+use function is_string;
+/**
+ * Class Job
+ * @package Grav\Common\Scheduler
+ */
class Job
{
use IntervalTrait;
+ /** @var string */
private $id;
- private $enabled = true;
+ /** @var bool */
+ private $enabled;
+ /** @var callable|string */
private $command;
+ /** @var string */
private $at;
+ /** @var array */
private $args = [];
+ /** @var bool */
private $runInBackground = true;
+ /** @var DateTime */
private $creationTime;
+ /** @var CronExpression */
private $executionTime;
+ /** @var string */
private $tempDir;
+ /** @var string */
private $lockFile;
+ /** @var bool */
private $truthTest = true;
+ /** @var string */
private $output;
+ /** @var int */
private $returnCode = 0;
+ /** @var array */
private $outputTo = [];
+ /** @var array */
private $emailTo = [];
+ /** @var array */
private $emailConfig = [];
+ /** @var callable|null */
private $before;
+ /** @var callable|null */
private $after;
+ /** @var callable */
private $whenOverlapping;
+ /** @var string */
private $outputMode;
+ /** @var Process|null $process */
private $process;
+ /** @var bool */
private $successful = false;
+ /** @var string|null */
private $backlink;
/**
@@ -46,7 +83,7 @@ class Job
*
* @param string|callable $command
* @param array $args
- * @param string $id
+ * @param string|null $id
*/
public function __construct($command, $args = [], $id = null)
{
@@ -60,7 +97,7 @@ class Job
$this->id = spl_object_hash($command);
}
}
- $this->creationTime = new \DateTime('now');
+ $this->creationTime = new DateTime('now');
// initialize the directory path for lock files
$this->tempDir = sys_get_temp_dir();
$this->command = $command;
@@ -73,7 +110,7 @@ class Job
/**
* Get the command
*
- * @return string
+ * @return Closure|string
*/
public function getCommand()
{
@@ -107,13 +144,16 @@ class Job
*/
public function getArguments()
{
- if (\is_string($this->args)) {
+ if (is_string($this->args)) {
return $this->args;
}
return null;
}
+ /**
+ * @return CronExpression
+ */
public function getCronExpression()
{
return CronExpression::factory($this->at);
@@ -145,10 +185,10 @@ class Job
* the job is due. Defaults to job creation time.
* It also default the execution time if not previously defined.
*
- * @param \DateTime $date
+ * @param DateTime|null $date
* @return bool
*/
- public function isDue(\DateTime $date = null)
+ public function isDue(DateTime $date = null)
{
// The execution time is being defaulted if not defined
if (!$this->executionTime) {
@@ -187,9 +227,8 @@ class Job
/**
* Sets/Gets an option backlink
*
- * @param string $link
- *
- * @return null|string
+ * @param string|null $link
+ * @return string|null
*/
public function backlink($link = null)
{
@@ -216,8 +255,8 @@ class Job
* being executed if the previous is still running.
* The job id is used as a filename for the lock file.
*
- * @param string $tempDir The directory path for the lock files
- * @param callable $whenOverlapping A callback to ignore job overlapping
+ * @param string|null $tempDir The directory path for the lock files
+ * @param callable|null $whenOverlapping A callback to ignore job overlapping
* @return self
*/
public function onlyOne($tempDir = null, callable $whenOverlapping = null)
@@ -232,7 +271,7 @@ class Job
if ($whenOverlapping) {
$this->whenOverlapping = $whenOverlapping;
} else {
- $this->whenOverlapping = function () {
+ $this->whenOverlapping = static function () {
return false;
};
}
@@ -298,8 +337,8 @@ class Job
if (is_callable($this->command)) {
$this->output = $this->exec();
} else {
- $args = \is_string($this->args) ? $this->args : implode(' ', $this->args);
- $command = $this->command . ' ' . $args;
+ $args = is_string($this->args) ? explode(' ', $this->args) : $this->args;
+ $command = array_merge([$this->command], $args);
$process = new Process($command);
$this->process = $process;
@@ -322,7 +361,6 @@ class Job
*/
public function finalize()
{
- /** @var Process $process */
$process = $this->process;
if ($process) {
@@ -344,13 +382,17 @@ class Job
/**
* Things to run after job has run
+ *
+ * @return void
*/
private function postRun()
{
if (count($this->outputTo) > 0) {
foreach ($this->outputTo as $file) {
$output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
- file_put_contents($file, $this->output, $output_mode);
+ $timestamp = (new DateTime('now'))->format('c');
+ $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
+ file_put_contents($file, $output, $output_mode);
}
}
@@ -374,7 +416,7 @@ class Job
private function createLockFile($content = null)
{
if ($this->lockFile) {
- if ($content === null || !\is_string($content)) {
+ if ($content === null || !is_string($content)) {
$content = $this->getId();
}
file_put_contents($this->lockFile, $content);
@@ -396,8 +438,8 @@ class Job
/**
* Execute a callable job.
*
- * @throws \RuntimeException
* @return string
+ * @throws RuntimeException
*/
private function exec()
{
@@ -406,12 +448,15 @@ class Job
try {
$return_data = call_user_func_array($this->command, $this->args);
$this->successful = true;
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
+ $return_data = $e->getMessage();
$this->successful = false;
}
$this->output = ob_get_clean() . (is_string($return_data) ? $return_data : '');
$this->postRun();
+
+ return $this->output;
}
/**
@@ -450,7 +495,7 @@ class Job
public function email($email)
{
if (!is_string($email) && !is_array($email)) {
- throw new \InvalidArgumentException('The email can be only string or array');
+ throw new InvalidArgumentException('The email can be only string or array');
}
$this->emailTo = is_array($email) ? $email : [$email];
@@ -519,4 +564,3 @@ class Job
return $this;
}
}
-
diff --git a/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php
index df5602ea..73a07122 100644
--- a/system/src/Grav/Common/Scheduler/Scheduler.php
+++ b/system/src/Grav/Common/Scheduler/Scheduler.php
@@ -3,33 +3,51 @@
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
+use DateTime;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Utils;
+use InvalidArgumentException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use RocketTheme\Toolbox\File\YamlFile;
+use function is_callable;
+use function is_string;
+/**
+ * Class Scheduler
+ * @package Grav\Common\Scheduler
+ */
class Scheduler
{
- /**
- * The queued jobs.
- *
- * @var array
- */
+ /** @var Job[] The queued jobs. */
private $jobs = [];
+
+ /** @var Job[] */
private $saved_jobs = [];
+
+ /** @var Job[] */
private $executed_jobs = [];
+
+ /** @var Job[] */
private $failed_jobs = [];
+
+ /** @var Job[] */
private $jobs_run = [];
+
+ /** @var array */
private $output_schedule = [];
+
+ /** @var array */
private $config;
+
+ /** @var string */
private $status_path;
/**
@@ -44,11 +62,12 @@ class Scheduler
if (!file_exists($this->status_path)) {
Folder::create($this->status_path);
}
-
}
/**
* Load saved jobs from config/scheduler.yaml file
+ *
+ * @return $this
*/
public function loadSavedJobs()
{
@@ -65,7 +84,7 @@ class Scheduler
}
if (isset($j['output'])) {
- $mode = isset($j['output_mode']) && $j['output_mode'] === 'append' ? true : false;
+ $mode = isset($j['output_mode']) && $j['output_mode'] === 'append';
$job->output($j['output'], $mode);
}
@@ -98,7 +117,6 @@ class Scheduler
$foreground[] = $job;
}
}
-
}
return [$background, $foreground];
}
@@ -106,21 +124,38 @@ class Scheduler
/**
* Get all jobs if they are disabled or not as one array
*
- * @return array
+ * @return Job[]
*/
public function getAllJobs()
{
- list($background, $foreground) = $this->loadSavedJobs()->getQueuedJobs(true);
+ [$background, $foreground] = $this->loadSavedJobs()->getQueuedJobs(true);
return array_merge($background, $foreground);
}
+ /**
+ * Get a specific Job based on id
+ *
+ * @param string $jobid
+ * @return Job|null
+ */
+ public function getJob($jobid)
+ {
+ $all = $this->getAllJobs();
+ foreach ($all as $job) {
+ if ($jobid == $job->getId()) {
+ return $job;
+ }
+ }
+ return null;
+ }
+
/**
* Queues a PHP function execution.
*
* @param callable $fn The function to execute
* @param array $args Optional arguments to pass to the php script
- * @param string $id Optional custom identifier
+ * @param string|null $id Optional custom identifier
* @return Job
*/
public function addFunction(callable $fn, $args = [], $id = null)
@@ -136,7 +171,7 @@ class Scheduler
*
* @param string $command The command to execute
* @param array $args Optional arguments to pass to the command
- * @param string $id Optional custom identifier
+ * @param string|null $id Optional custom identifier
* @return Job
*/
public function addCommand($command, $args = [], $id = null)
@@ -150,29 +185,30 @@ class Scheduler
/**
* Run the scheduler.
*
- * @param \DateTime|null $runTime Optional, run at specific moment
+ * @param DateTime|null $runTime Optional, run at specific moment
+ * @param bool $force force run even if not due
*/
- public function run(\DateTime $runTime = null)
+ public function run(DateTime $runTime = null, $force = false)
{
$this->loadSavedJobs();
- list($background, $foreground) = $this->getQueuedJobs(false);
+ [$background, $foreground] = $this->getQueuedJobs(false);
$alljobs = array_merge($background, $foreground);
if (null === $runTime) {
- $runTime = new \DateTime('now');
+ $runTime = new DateTime('now');
}
// Star processing jobs
foreach ($alljobs as $job) {
- if ($job->isDue($runTime)) {
+ if ($job->isDue($runTime) || $force) {
$job->run();
$this->jobs_run[] = $job;
}
}
// Finish handling any background jobs
- foreach($background as $job) {
+ foreach ($background as $job) {
$job->finalize();
}
@@ -184,6 +220,8 @@ class Scheduler
* Reset all collected data of last run.
*
* Call before run() if you call run() multiple times.
+ *
+ * @return $this
*/
public function resetRun()
{
@@ -199,7 +237,7 @@ class Scheduler
* Get the scheduler verbose output.
*
* @param string $type Allowed: text, html, array
- * @return mixed The return depends on the requested $type
+ * @return string|array The return depends on the requested $type
*/
public function getVerboseOutput($type = 'text')
{
@@ -211,12 +249,14 @@ class Scheduler
case 'array':
return $this->output_schedule;
default:
- throw new \InvalidArgumentException('Invalid output type');
+ throw new InvalidArgumentException('Invalid output type');
}
}
/**
* Remove all queued Jobs.
+ *
+ * @return $this
*/
public function clearJobs()
{
@@ -231,28 +271,44 @@ class Scheduler
* @return string
*/
public function getCronCommand()
+ {
+ $command = $this->getSchedulerCommand();
+
+ return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -";
+ }
+
+ /**
+ * @param string|null $php
+ * @return string
+ */
+ public function getSchedulerCommand($php = null)
{
$phpBinaryFinder = new PhpExecutableFinder();
- $php = $phpBinaryFinder->find();
+ $php = $php ?? $phpBinaryFinder->find();
$command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler';
- return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -";
+ return $command;
}
/**
* Helper to determine if cron job is setup
+ * 0 - Crontab Not found
+ * 1 - Crontab Found
+ * 2 - Error
*
* @return int
*/
public function isCrontabSetup()
{
- $process = new Process('crontab -l');
+ $process = new Process(['crontab', '-l']);
$process->run();
if ($process->isSuccessful()) {
$output = $process->getOutput();
+ $command = str_replace('/', '\/', $this->getSchedulerCommand('.*'));
+ $full_command = '/^(?!#).* .* .* .* .* ' . $command . '/m';
- return preg_match('$bin\/grav schedule$', $output) ? 1 : 0;
+ return preg_match($full_command, $output) ? 1 : 0;
}
$error = $process->getErrorOutput();
@@ -263,7 +319,7 @@ class Scheduler
/**
* Get the Job states file
*
- * @return \RocketTheme\Toolbox\File\FileInterface|YamlFile
+ * @return YamlFile
*/
public function getJobStates()
{
@@ -272,6 +328,8 @@ class Scheduler
/**
* Save job states to statys file
+ *
+ * @return void
*/
private function saveJobStates()
{
@@ -292,6 +350,24 @@ class Scheduler
$saved_states->save(array_merge($saved_states->content(), $new_states));
}
+ /**
+ * Try to determine who's running the process
+ *
+ * @return false|string
+ */
+ public function whoami()
+ {
+ $process = new Process('whoami');
+ $process->run();
+
+ if ($process->isSuccessful()) {
+ return trim($process->getOutput());
+ }
+
+ return $process->getErrorOutput();
+ }
+
+
/**
* Queue a job for execution in the correct queue.
*
@@ -313,7 +389,7 @@ class Scheduler
*/
private function addSchedulerVerboseOutput($string)
{
- $now = '[' . (new \DateTime('now'))->format('c') . '] ';
+ $now = '[' . (new DateTime('now'))->format('c') . '] ';
$this->output_schedule[] = $now . $string;
// Print to stdoutput in light gray
// echo "\033[37m{$string}\033[0m\n";
@@ -332,7 +408,7 @@ class Scheduler
$args = $job->getArguments();
// If callable, log the string Closure
if (is_callable($command)) {
- $command = \is_string($command) ? $command : 'Closure';
+ $command = is_string($command) ? $command : 'Closure';
}
$this->addSchedulerVerboseOutput("Success : {$command} {$args} ");
@@ -351,7 +427,7 @@ class Scheduler
$command = $job->getCommand();
// If callable, log the string Closure
if (is_callable($command)) {
- $command = \is_string($command) ? $command : 'Closure';
+ $command = is_string($command) ? $command : 'Closure';
}
$output = trim($job->getOutput());
$this->addSchedulerVerboseOutput("Error : {$command} → {$output} ");
diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php
index 0f610979..833e6480 100644
--- a/system/src/Grav/Common/Security.php
+++ b/system/src/Grav/Common/Security.php
@@ -3,17 +3,82 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use Exception;
+use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
use Grav\Common\Page\Pages;
+use Rhukster\DomSanitizer\DOMSanitizer;
+use function chr;
+use function count;
+use function is_array;
+use function is_string;
+/**
+ * Class Security
+ * @package Grav\Common
+ */
class Security
{
+ /**
+ * Sanitize SVG string for XSS code
+ *
+ * @param string $svg
+ * @return string
+ */
+ public static function sanitizeSvgString(string $svg): string
+ {
+ if (Grav::instance()['config']->get('security.sanitize_svg')) {
+ $sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
+ $sanitized = $sanitizer->sanitize($svg);
+ if (is_string($sanitized)) {
+ $svg = $sanitized;
+ }
+ }
+
+ return $svg;
+ }
+
+ /**
+ * Sanitize SVG for XSS code
+ *
+ * @param string $file
+ * @return void
+ */
+ public static function sanitizeSVG(string $file): void
+ {
+ if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) {
+ $sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
+ $original_svg = file_get_contents($file);
+ $clean_svg = $sanitizer->sanitize($original_svg);
+
+ // Quarantine bad SVG files and throw exception
+ if ($clean_svg !== false ) {
+ file_put_contents($file, $clean_svg);
+ } else {
+ $quarantine_file = basename($file);
+ $quarantine_dir = 'log://quarantine';
+ Folder::mkdir($quarantine_dir);
+ file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
+ unlink($file);
+ throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
+ }
+ }
+ }
+ /**
+ * Detect XSS code in Grav pages
+ *
+ * @param Pages $pages
+ * @param bool $route
+ * @param callable|null $status
+ * @return array
+ */
public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null)
{
$routes = $pages->routes();
@@ -30,7 +95,6 @@ class Security
]);
foreach ($routes as $path) {
-
$status && $status([
'type' => 'progress',
]);
@@ -43,7 +107,7 @@ class Security
$content = $page->value('content');
$data = ['header' => $header, 'content' => $content];
- $results = Security::detectXssFromArray($data);
+ $results = static::detectXssFromArray($data);
if (!empty($results)) {
if ($route) {
@@ -51,10 +115,8 @@ class Security
} else {
$list[$page->filePathClean()] = $results;
}
-
}
-
- } catch (\Exception $e) {
+ } catch (Exception $e) {
continue;
}
}
@@ -63,19 +125,25 @@ class Security
}
/**
+ * Detect XSS in an array or strings such as $_POST or $_GET
+ *
* @param array $array Array such as $_POST or $_GET
+ * @param array|null $options Extra options to be passed.
* @param string $prefix Prefix for returned values.
* @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
*/
- public static function detectXssFromArray(array $array, $prefix = '')
+ public static function detectXssFromArray(array $array, string $prefix = '', array $options = null)
{
- $list = [];
+ if (null === $options) {
+ $options = static::getXssDefaults();
+ }
+ $list = [];
foreach ($array as $key => $value) {
- if (\is_array($value)) {
- $list[] = static::detectXssFromArray($value, $prefix . $key . '.');
+ if (is_array($value)) {
+ $list[] = static::detectXssFromArray($value, $prefix . $key . '.', $options);
}
- if ($result = static::detectXss($value)) {
+ if ($result = static::detectXss($value, $options)) {
$list[] = [$prefix . $key => $result];
}
}
@@ -89,19 +157,39 @@ class Security
/**
* Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
+ *
* return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
* their content.
*
- * @param string $string The string to run XSS detection logic on
- * @return bool|string Type of XSS vector if the given `$string` may contain XSS, false otherwise.
+ * @param string|null $string The string to run XSS detection logic on
+ * @param array|null $options
+ * @return string|null Type of XSS vector if the given `$string` may contain XSS, false otherwise.
*
* Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138
*/
- public static function detectXss($string)
+ public static function detectXss($string, array $options = null): ?string
{
// Skip any null or non string values
- if (null === $string || !\is_string($string) || empty($string)) {
- return false;
+ if (null === $string || !is_string($string) || empty($string)) {
+ return null;
+ }
+
+ if (null === $options) {
+ $options = static::getXssDefaults();
+ }
+
+ $enabled_rules = (array)($options['enabled_rules'] ?? null);
+ $dangerous_tags = (array)($options['dangerous_tags'] ?? null);
+ if (!$dangerous_tags) {
+ $enabled_rules['dangerous_tags'] = false;
+ }
+ $invalid_protocols = (array)($options['invalid_protocols'] ?? null);
+ if (!$invalid_protocols) {
+ $enabled_rules['invalid_protocols'] = false;
+ }
+ $enabled_rules = array_filter($enabled_rules, static function ($val) { return !empty($val); });
+ if (!$enabled_rules) {
+ return null;
}
// Keep a copy of the original string before cleaning up
@@ -111,32 +199,26 @@ class Security
$string = urldecode($string);
// Convert Hexadecimals
- $string = (string)preg_replace_callback('!(|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) {
- return \chr(hexdec($m[2]));
+ $string = (string)preg_replace_callback('!(|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) {
+ return chr(hexdec($m[2]));
}, $string);
// Clean up entities
- $string = preg_replace('!(+[0-9]+)!u','$1;', $string);
+ $string = preg_replace('!(+[0-9]+)!u', '$1;', $string);
// Decode entities
$string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
// Strip whitespace characters
- $string = preg_replace('!\s!u','', $string);
-
- $config = Grav::instance()['config'];
-
- $dangerous_tags = array_map('preg_quote', array_map("trim", $config->get('security.xss_dangerous_tags')));
- $invalid_protocols = array_map('preg_quote', array_map("trim", $config->get('security.xss_invalid_protocols')));
- $enabled_rules = $config->get('security.xss_enabled');
+ $string = preg_replace('!\s!u', '', $string);
// Set the patterns we'll test against
$patterns = [
// Match any attribute starting with "on" or xmlns
- 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])(\son|\sxmlns)[a-z].*=>?#iUu',
+ 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])([\s\/]on|\sxmlns)[a-z].*=>?#iUu',
// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
- 'invalid_protocols' => '#(' . implode('|', $invalid_protocols) . '):.*?#iUu',
+ 'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):\S.*?#iUu',
// Match -moz-bindings
'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u',
@@ -145,21 +227,30 @@ class Security
'html_inline_styles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>]*(url\:|x\:expression).*)>?#iUu',
// Match potentially dangerous tags
- 'dangerous_tags' => '#*(' . implode('|', $dangerous_tags) . ')[^>]*>?#ui'
+ 'dangerous_tags' => '#*(' . implode('|', array_map('preg_quote', $dangerous_tags, ['#'])) . ')[^>]*>?#ui'
];
-
// Iterate over rules and return label if fail
- foreach ((array) $patterns as $name => $regex) {
- if ($enabled_rules[$name] === true) {
-
+ foreach ($patterns as $name => $regex) {
+ if (!empty($enabled_rules[$name])) {
if (preg_match($regex, $string) || preg_match($regex, $orig)) {
return $name;
}
-
}
}
- return false;
+ return null;
+ }
+
+ public static function getXssDefaults(): array
+ {
+ /** @var Config $config */
+ $config = Grav::instance()['config'];
+
+ return [
+ 'enabled_rules' => $config->get('security.xss_enabled'),
+ 'dangerous_tags' => array_map('trim', $config->get('security.xss_dangerous_tags')),
+ 'invalid_protocols' => array_map('trim', $config->get('security.xss_invalid_protocols')),
+ ];
}
}
diff --git a/system/src/Grav/Common/Service/AccountsServiceProvider.php b/system/src/Grav/Common/Service/AccountsServiceProvider.php
index e0778beb..8b158c93 100644
--- a/system/src/Grav/Common/Service/AccountsServiceProvider.php
+++ b/system/src/Grav/Common/Service/AccountsServiceProvider.php
@@ -3,127 +3,155 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Service;
use Grav\Common\Config\Config;
-use Grav\Common\Debugger;
+use Grav\Common\Grav;
+use Grav\Common\Page\Header;
+use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\User\DataUser;
-use Grav\Common\User\FlexUser;
use Grav\Common\User\User;
-use Grav\Framework\File\Formatter\YamlFormatter;
+use Grav\Events\PermissionsRegisterEvent;
+use Grav\Framework\Acl\Permissions;
+use Grav\Framework\Acl\PermissionsReader;
use Grav\Framework\Flex\Flex;
-use Grav\Framework\Flex\FlexDirectory;
+use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Event\Event;
-use RocketTheme\Toolbox\Event\EventDispatcher;
+use SplFileInfo;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use function define;
+use function defined;
+use function is_array;
+/**
+ * Class AccountsServiceProvider
+ * @package Grav\Common\Service
+ */
class AccountsServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
- $container['accounts'] = function (Container $container) {
- $type = strtolower(defined('GRAV_USER_INSTANCE') ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'data'));
- if ($type === 'flex') {
- /** @var Debugger $debugger */
- $debugger = $container['debugger'];
- $debugger->addMessage('User Accounts: Flex Directory');
- return $this->flexAccounts($container);
+ $container['permissions'] = static function (Grav $container) {
+ /** @var Config $config */
+ $config = $container['config'];
+
+ $permissions = new Permissions();
+ $permissions->addTypes($config->get('permissions.types', []));
+
+ $array = $config->get('permissions.actions');
+ if (is_array($array)) {
+ $actions = PermissionsReader::fromArray($array, $permissions->getTypes());
+ $permissions->addActions($actions);
}
- return $this->dataAccounts($container);
+ $event = new PermissionsRegisterEvent($permissions);
+ $container->dispatchEvent($event);
+
+ return $permissions;
+ };
+
+ $container['accounts'] = function (Container $container) {
+ $type = $this->initialize($container);
+
+ return $type === 'flex' ? $this->flexAccounts($container) : $this->regularAccounts($container);
+ };
+
+ $container['user_groups'] = static function (Container $container) {
+ /** @var Flex $flex */
+ $flex = $container['flex'];
+ $directory = $flex->getDirectory('user-groups');
+
+ return $directory ? $directory->getIndex() : null;
};
- $container['users'] = $container->factory(function (Container $container) {
+ $container['users'] = $container->factory(static function (Container $container) {
user_error('Grav::instance()[\'users\'] is deprecated since Grav 1.6, use Grav::instance()[\'accounts\'] instead', E_USER_DEPRECATED);
return $container['accounts'];
});
}
- protected function dataAccounts(Container $container)
+ /**
+ * @param Container $container
+ * @return string
+ */
+ protected function initialize(Container $container): string
{
- if (!defined('GRAV_USER_INSTANCE')) {
- define('GRAV_USER_INSTANCE', 'DATA');
- }
+ $isDefined = defined('GRAV_USER_INSTANCE');
+ $type = strtolower($isDefined ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'regular'));
- // Use User class for backwards compatibility.
- return new DataUser\UserCollection(User::class);
- }
+ if ($type === 'flex') {
+ if (!$isDefined) {
+ define('GRAV_USER_INSTANCE', 'FLEX');
+ }
- protected function flexAccounts(Container $container)
- {
- if (!defined('GRAV_USER_INSTANCE')) {
- define('GRAV_USER_INSTANCE', 'FLEX');
+ /** @var EventDispatcher $dispatcher */
+ $dispatcher = $container['events'];
+
+ // Stop /admin/user from working, display error instead.
+ $dispatcher->addListener(
+ 'onAdminPage',
+ static function (Event $event) {
+ $grav = Grav::instance();
+ $admin = $grav['admin'];
+ [$base,$location,] = $admin->getRouteDetails();
+ if ($location !== 'user' || isset($grav['flex_objects'])) {
+ return;
+ }
+
+ /** @var PageInterface $page */
+ $page = $event['page'];
+ $page->init(new SplFileInfo('plugin://admin/pages/admin/error.md'));
+ $page->routable(true);
+ $header = $page->header();
+ $header->title = 'Please install missing plugin';
+ $page->content("## Please install and enable **[Flex Objects]({$base}/plugins/flex-objects)** plugin. It is required to edit **Flex User Accounts**.");
+
+ /** @var Header $header */
+ $header = $page->header();
+ $directory = $grav['accounts']->getFlexDirectory();
+ $menu = $directory->getConfig('admin.menu.list');
+ $header->access = $menu['authorize'] ?? ['admin.super'];
+ },
+ 100000
+ );
+ } elseif (!$isDefined) {
+ define('GRAV_USER_INSTANCE', 'REGULAR');
}
- /** @var Config $config */
- $config = $container['config'];
-
- $options = [
- 'enabled' => true,
- 'data' => [
- 'object' => User::class, // Use User class for backwards compatibility.
- 'collection' => FlexUser\UserCollection::class,
- 'index' => FlexUser\UserIndex::class,
- 'storage' => $this->getFlexStorage($config->get('system.accounts.storage', 'file')),
- 'search' => [
- 'options' => [
- 'contains' => 1
- ],
- 'fields' => [
- 'key',
- 'email'
- ]
- ]
- ]
- ] + ($config->get('plugins.flex-objects.object') ?: []);
-
- $directory = new FlexDirectory('accounts', 'blueprints://user/accounts.yaml', $options);
-
- /** @var EventDispatcher $dispatcher */
- $dispatcher = $container['events'];
- $dispatcher->addListener('onFlexInit', function (Event $event) use ($directory) {
- /** @var Flex $flex */
- $flex = $event['flex'];
- $flex->addDirectory($directory);
- });
-
- return $directory->getIndex();
+ return $type;
}
- protected function getFlexStorage($config)
+ /**
+ * @param Container $container
+ * @return DataUser\UserCollection
+ */
+ protected function regularAccounts(Container $container)
{
- if (\is_array($config)) {
- return $config;
- }
+ // Use User class for backwards compatibility.
+ return new DataUser\UserCollection(User::class);
+ }
- if ($config === 'folder') {
- return [
- 'class' => FlexUser\Storage\UserFolderStorage::class,
- 'options' => [
- 'formatter' => ['class' => YamlFormatter::class],
- 'folder' => 'account://',
- 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/user.yaml',
- 'key' => 'username',
- 'indexed' => true
- ],
- ];
- }
+ /**
+ * @param Container $container
+ * @return FlexIndexInterface|null
+ */
+ protected function flexAccounts(Container $container)
+ {
+ /** @var Flex $flex */
+ $flex = $container['flex'];
+ $directory = $flex->getDirectory('user-accounts');
- return [
- 'class' => FlexUser\Storage\UserFileStorage::class,
- 'options' => [
- 'formatter' => ['class' => YamlFormatter::class],
- 'folder' => 'account://',
- 'pattern' => '{FOLDER}/{KEY}.yaml',
- 'key' => 'storage_key',
- 'indexed' => true
- ],
- ];
+ return $directory ? $directory->getIndex() : null;
}
}
diff --git a/system/src/Grav/Common/Service/AssetsServiceProvider.php b/system/src/Grav/Common/Service/AssetsServiceProvider.php
index 004da9ca..1e4e647c 100644
--- a/system/src/Grav/Common/Service/AssetsServiceProvider.php
+++ b/system/src/Grav/Common/Service/AssetsServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Grav\Common\Assets;
+/**
+ * Class AssetsServiceProvider
+ * @package Grav\Common\Service
+ */
class AssetsServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['assets'] = function () {
diff --git a/system/src/Grav/Common/Service/BackupsServiceProvider.php b/system/src/Grav/Common/Service/BackupsServiceProvider.php
index 2c01036e..00fa9631 100644
--- a/system/src/Grav/Common/Service/BackupsServiceProvider.php
+++ b/system/src/Grav/Common/Service/BackupsServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Grav\Common\Backup\Backups;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class BackupsServiceProvider
+ * @package Grav\Common\Service
+ */
class BackupsServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['backups'] = function () {
diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php
index c874441f..b56f62fd 100644
--- a/system/src/Grav/Common/Service/ConfigServiceProvider.php
+++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php
@@ -3,12 +3,13 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Service;
+use DirectoryIterator;
use Grav\Common\Config\CompiledBlueprints;
use Grav\Common\Config\CompiledConfig;
use Grav\Common\Config\CompiledLanguages;
@@ -16,13 +17,22 @@ use Grav\Common\Config\Config;
use Grav\Common\Config\ConfigFileFinder;
use Grav\Common\Config\Setup;
use Grav\Common\Language\Language;
+use Grav\Framework\Mime\MimeTypes;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+/**
+ * Class ConfigServiceProvider
+ * @package Grav\Common\Service
+ */
class ConfigServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['setup'] = function ($c) {
@@ -47,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
return $config;
};
+ $container['mime'] = function ($c) {
+ /** @var Config $config */
+ $config = $c['config'];
+ $mimes = $config->get('mime.types', []);
+ foreach ($config->get('media.types', []) as $ext => $media) {
+ if (!empty($media['mime'])) {
+ $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
+ }
+ }
+
+ return MimeTypes::createFromMimes($mimes);
+ };
+
$container['languages'] = function ($c) {
return static::languages($c);
};
@@ -56,6 +79,10 @@ class ConfigServiceProvider implements ServiceProviderInterface
};
}
+ /**
+ * @param Container $container
+ * @return mixed
+ */
public static function blueprints(Container $container)
{
/** Setup $setup */
@@ -71,6 +98,8 @@ class ConfigServiceProvider implements ServiceProviderInterface
$files += (new ConfigFileFinder)->locateFiles($paths);
$paths = $locator->findResources('plugins://');
$files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints');
+ $paths = $locator->findResources('themes://');
+ $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths, 'blueprints');
$blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT);
@@ -96,9 +125,11 @@ class ConfigServiceProvider implements ServiceProviderInterface
$files += (new ConfigFileFinder)->locateFiles($paths);
$paths = $locator->findResources('plugins://');
$files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths);
+ $paths = $locator->findResources('themes://');
+ $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths);
$compiled = new CompiledConfig($cache, $files, GRAV_ROOT);
- $compiled->setBlueprints(function() use ($container) {
+ $compiled->setBlueprints(function () use ($container) {
return $container['blueprints'];
});
@@ -108,6 +139,10 @@ class ConfigServiceProvider implements ServiceProviderInterface
return $config;
}
+ /**
+ * @param Container $container
+ * @return mixed
+ */
public static function languages(Container $container)
{
/** @var Setup $setup */
@@ -149,9 +184,9 @@ class ConfigServiceProvider implements ServiceProviderInterface
$paths = [];
foreach ($plugins as $path) {
- $iterator = new \DirectoryIterator($path);
+ $iterator = new DirectoryIterator($path);
- /** @var \DirectoryIterator $directory */
+ /** @var DirectoryIterator $directory */
foreach ($iterator as $directory) {
if (!$directory->isDir() || $directory->isDot()) {
continue;
@@ -168,5 +203,4 @@ class ConfigServiceProvider implements ServiceProviderInterface
}
return $paths;
}
-
}
diff --git a/system/src/Grav/Common/Service/ErrorServiceProvider.php b/system/src/Grav/Common/Service/ErrorServiceProvider.php
index fce69ff5..32273658 100644
--- a/system/src/Grav/Common/Service/ErrorServiceProvider.php
+++ b/system/src/Grav/Common/Service/ErrorServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Grav\Common\Errors\Errors;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class ErrorServiceProvider
+ * @package Grav\Common\Service
+ */
class ErrorServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['errors'] = new Errors;
diff --git a/system/src/Grav/Common/Service/FilesystemServiceProvider.php b/system/src/Grav/Common/Service/FilesystemServiceProvider.php
index f5547cb2..62dad5d8 100644
--- a/system/src/Grav/Common/Service/FilesystemServiceProvider.php
+++ b/system/src/Grav/Common/Service/FilesystemServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Grav\Framework\Filesystem\Filesystem;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class FilesystemServiceProvider
+ * @package Grav\Common\Service
+ */
class FilesystemServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['filesystem'] = function () {
diff --git a/system/src/Grav/Common/Service/FlexServiceProvider.php b/system/src/Grav/Common/Service/FlexServiceProvider.php
new file mode 100644
index 00000000..9c8cd49b
--- /dev/null
+++ b/system/src/Grav/Common/Service/FlexServiceProvider.php
@@ -0,0 +1,117 @@
+ $config->get('system.flex', [])]);
+ FlexFormFlash::setFlex($flex);
+
+ $accountsEnabled = $config->get('system.accounts.type', 'regular') === 'flex';
+ $pagesEnabled = $config->get('system.pages.type', 'regular') === 'flex';
+
+ // Add built-in types from Grav.
+ if ($pagesEnabled) {
+ $flex->addDirectoryType(
+ 'pages',
+ 'blueprints://flex/pages.yaml',
+ [
+ 'enabled' => $pagesEnabled
+ ]
+ );
+ }
+ if ($accountsEnabled) {
+ $flex->addDirectoryType(
+ 'user-accounts',
+ 'blueprints://flex/user-accounts.yaml',
+ [
+ 'enabled' => $accountsEnabled,
+ 'data' => [
+ 'storage' => $this->getFlexAccountsStorage($config),
+ ]
+ ]
+ );
+ $flex->addDirectoryType(
+ 'user-groups',
+ 'blueprints://flex/user-groups.yaml',
+ [
+ 'enabled' => $accountsEnabled
+ ]
+ );
+ }
+
+ // Call event to register Flex Directories.
+ $event = new FlexRegisterEvent($flex);
+ $container->dispatchEvent($event);
+
+ return $flex;
+ };
+ }
+
+ /**
+ * @param Config $config
+ * @return array
+ */
+ private function getFlexAccountsStorage(Config $config): array
+ {
+ $value = $config->get('system.accounts.storage', 'file');
+ if (is_array($value)) {
+ return $value;
+ }
+
+ if ($value === 'folder') {
+ return [
+ 'class' => UserFolderStorage::class,
+ 'options' => [
+ 'file' => 'user',
+ 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}',
+ 'key' => 'storage_key'
+ ],
+ ];
+ }
+
+ if ($value === 'file') {
+ return [
+ 'class' => UserFileStorage::class,
+ 'options' => [
+ 'pattern' => '{FOLDER}/{KEY}{EXT}',
+ 'key' => 'username'
+ ],
+ ];
+ }
+
+ return [];
+ }
+}
diff --git a/system/src/Grav/Common/Service/InflectorServiceProvider.php b/system/src/Grav/Common/Service/InflectorServiceProvider.php
index fd695474..861a69e0 100644
--- a/system/src/Grav/Common/Service/InflectorServiceProvider.php
+++ b/system/src/Grav/Common/Service/InflectorServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Grav\Common\Inflector;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class InflectorServiceProvider
+ * @package Grav\Common\Service
+ */
class InflectorServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['inflector'] = function () {
diff --git a/system/src/Grav/Common/Service/LoggerServiceProvider.php b/system/src/Grav/Common/Service/LoggerServiceProvider.php
index 85830dee..5043cfbd 100644
--- a/system/src/Grav/Common/Service/LoggerServiceProvider.php
+++ b/system/src/Grav/Common/Service/LoggerServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,8 +15,16 @@ use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+/**
+ * Class LoggerServiceProvider
+ * @package Grav\Common\Service
+ */
class LoggerServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['log'] = function ($c) {
diff --git a/system/src/Grav/Common/Service/OutputServiceProvider.php b/system/src/Grav/Common/Service/OutputServiceProvider.php
index a9b2877e..9a49185a 100644
--- a/system/src/Grav/Common/Service/OutputServiceProvider.php
+++ b/system/src/Grav/Common/Service/OutputServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,8 +14,16 @@ use Grav\Common\Twig\Twig;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class OutputServiceProvider
+ * @package Grav\Common\Service
+ */
class OutputServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['output'] = function ($c) {
diff --git a/system/src/Grav/Common/Service/PagesServiceProvider.php b/system/src/Grav/Common/Service/PagesServiceProvider.php
index f425b6d9..55f6a450 100644
--- a/system/src/Grav/Common/Service/PagesServiceProvider.php
+++ b/system/src/Grav/Common/Service/PagesServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -17,20 +17,30 @@ use Grav\Common\Page\Pages;
use Grav\Common\Uri;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+use SplFileInfo;
+use function defined;
+/**
+ * Class PagesServiceProvider
+ * @package Grav\Common\Service
+ */
class PagesServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
- $container['pages'] = function ($c) {
- return new Pages($c);
+ $container['pages'] = function (Grav $grav) {
+ return new Pages($grav);
};
- if (\defined('GRAV_CLI')) {
- $container['page'] = static function ($c) {
- $path = $c['locator']->findResource('system://pages/notfound.md');
+ if (defined('GRAV_CLI')) {
+ $container['page'] = static function (Grav $grav) {
+ $path = $grav['locator']->findResource('system://pages/notfound.md');
$page = new Page();
- $page->init(new \SplFileInfo($path));
+ $page->init(new SplFileInfo($path));
$page->routable(false);
return $page;
@@ -39,17 +49,15 @@ class PagesServiceProvider implements ServiceProviderInterface
return;
}
- $container['page'] = function ($c) {
- /** @var Grav $c */
-
+ $container['page'] = static function (Grav $grav) {
/** @var Pages $pages */
- $pages = $c['pages'];
+ $pages = $grav['pages'];
/** @var Config $config */
- $config = $c['config'];
+ $config = $grav['config'];
/** @var Uri $uri */
- $uri = $c['uri'];
+ $uri = $grav['uri'];
$path = $uri->path() ?: '/'; // Don't trim to support trailing slash default routes
$page = $pages->dispatch($path);
@@ -58,55 +66,69 @@ class PagesServiceProvider implements ServiceProviderInterface
if ($page) {
// some debugger override logic
if ($page->debugger() === false) {
- $c['debugger']->enabled(false);
+ $grav['debugger']->enabled(false);
}
if ($config->get('system.force_ssl')) {
- if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
+ $scheme = $uri->scheme(true);
+ if ($scheme !== 'https') {
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
- $c->redirect($url);
+ $grav->redirect($url);
}
}
- $url = $pages->route($page->route());
+ $route = $page->route();
+ if ($route && \in_array($uri->method(), ['GET', 'HEAD'], true)) {
+ $pageExtension = $page->urlExtension();
+ $url = $pages->route($route) . $pageExtension;
+
+ if ($uri->params()) {
+ if ($url === '/') { //Avoid double slash
+ $url = $uri->params();
+ } else {
+ $url .= $uri->params();
+ }
+ }
+ if ($uri->query()) {
+ $url .= '?' . $uri->query();
+ }
+ if ($uri->fragment()) {
+ $url .= '#' . $uri->fragment();
+ }
+
+ /** @var Language $language */
+ $language = $grav['language'];
- if ($uri->params()) {
- if ($url === '/') { //Avoid double slash
- $url = $uri->params();
- } else {
- $url .= $uri->params();
+ $redirectCode = (int)$config->get('system.pages.redirect_default_route', 0);
+
+ // Language-specific redirection scenarios
+ if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
+ $grav->redirect($url, $redirectCode);
}
- }
- if ($uri->query()) {
- $url .= '?' . $uri->query();
- }
- if ($uri->fragment()) {
- $url .= '#' . $uri->fragment();
- }
- /** @var Language $language */
- $language = $c['language'];
+ // Default route test and redirect
+ if ($redirectCode) {
+ $uriExtension = $uri->extension();
+ $uriExtension = null !== $uriExtension ? '.' . $uriExtension : '';
- // Language-specific redirection scenarios
- if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
- $c->redirect($url);
- }
- // Default route test and redirect
- if ($config->get('system.pages.redirect_default_route') && $page->route() !== $path) {
- $c->redirect($url);
+ if ($route !== $path || ($pageExtension !== $uriExtension
+ && \in_array($pageExtension, ['', '.htm', '.html'], true)
+ && \in_array($uriExtension, ['', '.htm', '.html'], true))) {
+ $grav->redirect($url, $redirectCode);
+ }
+ }
}
}
// if page is not found, try some fallback stuff
if (!$page || !$page->routable()) {
-
// Try fallback URL stuff...
- $page = $c->fallbackUrl($path);
+ $page = $grav->fallbackUrl($path);
if (!$page) {
- $path = $c['locator']->findResource('system://pages/notfound.md');
+ $path = $grav['locator']->findResource('system://pages/notfound.md');
$page = new Page();
- $page->init(new \SplFileInfo($path));
+ $page->init(new SplFileInfo($path));
$page->routable(false);
}
}
diff --git a/system/src/Grav/Common/Service/RequestServiceProvider.php b/system/src/Grav/Common/Service/RequestServiceProvider.php
index 6410ef72..17b3149e 100644
--- a/system/src/Grav/Common/Service/RequestServiceProvider.php
+++ b/system/src/Grav/Common/Service/RequestServiceProvider.php
@@ -3,20 +3,36 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Service;
use Grav\Common\Uri;
+use JsonException;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+use function explode;
+use function fopen;
+use function function_exists;
+use function in_array;
+use function is_array;
+use function strtolower;
+use function trim;
+/**
+ * Class RequestServiceProvider
+ * @package Grav\Common\Service
+ */
class RequestServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['request'] = function () {
@@ -28,10 +44,59 @@ class RequestServiceProvider implements ServiceProviderInterface
$psr17Factory // StreamFactory
);
- return $creator->fromGlobals();
+ $server = $_SERVER;
+ if (false === isset($server['REQUEST_METHOD'])) {
+ $server['REQUEST_METHOD'] = 'GET';
+ }
+ $method = $server['REQUEST_METHOD'];
+
+ $headers = function_exists('getallheaders') ? getallheaders() : $creator::getHeadersFromServer($_SERVER);
+
+ $post = null;
+ if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
+ foreach ($headers as $headerName => $headerValue) {
+ if ('content-type' !== strtolower($headerName)) {
+ continue;
+ }
+
+ $contentType = strtolower(trim(explode(';', $headerValue, 2)[0]));
+ switch ($contentType) {
+ case 'application/x-www-form-urlencoded':
+ case 'multipart/form-data':
+ $post = $_POST;
+ break 2;
+ case 'application/json':
+ case 'application/vnd.api+json':
+ try {
+ $json = file_get_contents('php://input');
+ $post = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
+ if (!is_array($post)) {
+ $post = null;
+ }
+ } catch (JsonException $e) {
+ $post = null;
+ }
+ break 2;
+ }
+ }
+ }
+
+ // Remove _url from ngnix routes.
+ $get = $_GET;
+ unset($get['_url']);
+ if (isset($server['QUERY_STRING'])) {
+ $query = $server['QUERY_STRING'];
+ if (strpos($query, '_url=') !== false) {
+ parse_str($query, $query);
+ unset($query['_url']);
+ $server['QUERY_STRING'] = http_build_query($query);
+ }
+ }
+
+ return $creator->fromArrays($server, $headers, $_COOKIE, $get, $post, $_FILES, fopen('php://input', 'rb') ?: null);
};
- $container['route'] = $container->factory(function() {
+ $container['route'] = $container->factory(function () {
return clone Uri::getCurrentRoute();
});
}
diff --git a/system/src/Grav/Common/Service/SchedulerServiceProvider.php b/system/src/Grav/Common/Service/SchedulerServiceProvider.php
index 1df4bbcd..a9527254 100644
--- a/system/src/Grav/Common/Service/SchedulerServiceProvider.php
+++ b/system/src/Grav/Common/Service/SchedulerServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -13,8 +13,16 @@ use Grav\Common\Scheduler\Scheduler;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+/**
+ * Class SchedulerServiceProvider
+ * @package Grav\Common\Service
+ */
class SchedulerServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['scheduler'] = function () {
diff --git a/system/src/Grav/Common/Service/SessionServiceProvider.php b/system/src/Grav/Common/Service/SessionServiceProvider.php
index 84d23d35..88c833f2 100644
--- a/system/src/Grav/Common/Service/SessionServiceProvider.php
+++ b/system/src/Grav/Common/Service/SessionServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -14,16 +14,24 @@ use Grav\Common\Debugger;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\Utils;
+use Grav\Framework\Session\Messages;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
-use RocketTheme\Toolbox\Session\Message;
+/**
+ * Class SessionServiceProvider
+ * @package Grav\Common\Service
+ */
class SessionServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
// Define session service.
- $container['session'] = function ($c) {
+ $container['session'] = static function ($c) {
/** @var Config $config */
$config = $c['config'];
@@ -35,18 +43,23 @@ class SessionServiceProvider implements ServiceProviderInterface
$cookie_secure = (bool)$config->get('system.session.secure', false);
$cookie_httponly = (bool)$config->get('system.session.httponly', true);
$cookie_lifetime = (int)$config->get('system.session.timeout', 1800);
+ $cookie_domain = $config->get('system.session.domain');
$cookie_path = $config->get('system.session.path');
+ $cookie_samesite = $config->get('system.session.samesite', 'Lax');
+
+ if (null === $cookie_domain) {
+ $cookie_domain = $uri->host();
+ if ($cookie_domain === 'localhost') {
+ $cookie_domain = '';
+ }
+ }
+
if (null === $cookie_path) {
$cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/');
}
// Session cookie path requires trailing slash.
$cookie_path = rtrim($cookie_path, '/') . '/';
- $cookie_domain = $uri->host();
- if ($cookie_domain === 'localhost') {
- $cookie_domain = '';
- }
-
// Activate admin if we're inside the admin path.
$is_admin = false;
if ($config->get('plugins.admin.enabled')) {
@@ -87,7 +100,8 @@ class SessionServiceProvider implements ServiceProviderInterface
'cookie_path' => $cookie_path,
'cookie_domain' => $cookie_domain,
'cookie_secure' => $cookie_secure,
- 'cookie_httponly' => $cookie_httponly
+ 'cookie_httponly' => $cookie_httponly,
+ 'cookie_samesite' => $cookie_samesite
] + (array) $config->get('system.session.options');
$session = new Session($options);
@@ -103,14 +117,14 @@ class SessionServiceProvider implements ServiceProviderInterface
$debugger = $c['debugger'];
$debugger->addMessage('Inactive session: session messages may disappear', 'warming');
- return new Message;
+ return new Messages();
}
/** @var Session $session */
$session = $c['session'];
- if (!isset($session->messages)) {
- $session->messages = new Message;
+ if (!$session->messages instanceof Messages) {
+ $session->messages = new Messages();
}
return $session->messages;
diff --git a/system/src/Grav/Common/Service/StreamsServiceProvider.php b/system/src/Grav/Common/Service/StreamsServiceProvider.php
index 3bbbea1d..edde09ad 100644
--- a/system/src/Grav/Common/Service/StreamsServiceProvider.php
+++ b/system/src/Grav/Common/Service/StreamsServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -17,12 +17,20 @@ use RocketTheme\Toolbox\StreamWrapper\ReadOnlyStream;
use RocketTheme\Toolbox\StreamWrapper\Stream;
use RocketTheme\Toolbox\StreamWrapper\StreamBuilder;
+/**
+ * Class StreamsServiceProvider
+ * @package Grav\Common\Service
+ */
class StreamsServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
- $container['locator'] = function(Container $container) {
- $locator = new UniformResourceLocator(GRAV_ROOT);
+ $container['locator'] = function (Container $container) {
+ $locator = new UniformResourceLocator(GRAV_WEBROOT);
/** @var Setup $setup */
$setup = $container['setup'];
@@ -31,7 +39,7 @@ class StreamsServiceProvider implements ServiceProviderInterface
return $locator;
};
- $container['streams'] = function(Container $container) {
+ $container['streams'] = function (Container $container) {
/** @var Setup $setup */
$setup = $container['setup'];
diff --git a/system/src/Grav/Common/Service/TaskServiceProvider.php b/system/src/Grav/Common/Service/TaskServiceProvider.php
index c22121a9..49ce147e 100644
--- a/system/src/Grav/Common/Service/TaskServiceProvider.php
+++ b/system/src/Grav/Common/Service/TaskServiceProvider.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common\Service
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,13 +12,26 @@ namespace Grav\Common\Service;
use Grav\Common\Grav;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+use Psr\Http\Message\ServerRequestInterface;
+/**
+ * Class TaskServiceProvider
+ * @package Grav\Common\Service
+ */
class TaskServiceProvider implements ServiceProviderInterface
{
+ /**
+ * @param Container $container
+ * @return void
+ */
public function register(Container $container)
{
$container['task'] = function (Grav $c) {
- $task = $_POST['task'] ?? $c['uri']->param('task');
+ /** @var ServerRequestInterface $request */
+ $request = $c['request'];
+ $body = $request->getParsedBody();
+
+ $task = $body['task'] ?? $c['uri']->param('task');
if (null !== $task) {
$task = filter_var($task, FILTER_SANITIZE_STRING);
}
@@ -27,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
};
$container['action'] = function (Grav $c) {
- $action = $_POST['action'] ?? $c['uri']->param('action');
+ /** @var ServerRequestInterface $request */
+ $request = $c['request'];
+ $body = $request->getParsedBody();
+
+ $action = $body['action'] ?? $c['uri']->param('action');
if (null !== $action) {
$action = filter_var($action, FILTER_SANITIZE_STRING);
}
diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php
index cd9b2c17..32216270 100644
--- a/system/src/Grav/Common/Session.php
+++ b/system/src/Grav/Common/Session.php
@@ -3,14 +3,22 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use Grav\Common\Form\FormFlash;
+use Grav\Events\SessionStartEvent;
+use Grav\Plugin\Form\Forms;
+use JsonException;
+use function is_string;
+/**
+ * Class Session
+ * @package Grav\Common
+ */
class Session extends \Grav\Framework\Session\Session
{
/** @var bool */
@@ -31,6 +39,8 @@ class Session extends \Grav\Framework\Session\Session
* Initialize session.
*
* Code in this function has been moved into SessionServiceProvider class.
+ *
+ * @return void
*/
public function init()
{
@@ -68,7 +78,7 @@ class Session extends \Grav\Framework\Session\Session
/**
* Checks if the session was started.
*
- * @return Boolean
+ * @return bool
* @deprecated 1.5 Use ->isStarted() method instead.
*/
public function started()
@@ -102,7 +112,7 @@ class Session extends \Grav\Framework\Session\Session
{
$serialized = $this->__get($name);
- $object = \is_string($serialized) ? unserialize($serialized, ['allowed_classes' => true]) : $serialized;
+ $object = is_string($serialized) ? unserialize($serialized, ['allowed_classes' => true]) : $serialized;
$this->__unset($name);
@@ -118,14 +128,14 @@ class Session extends \Grav\Framework\Session\Session
/** @var Uri $uri */
$uri = $grav['uri'];
- /** @var Grav\Plugin\Form\Forms $form */
- $form = $grav['forms']->getActiveForm();
+ /** @var Forms|null $form */
+ $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line (form plugin)
$sessionField = base64_encode($uri->url);
- /** @var FormFlash $flash */
- $flash = $form ? $form->getFlash() : null;
- $object = $flash ? [$sessionField => $flash->getLegacyFiles()] : null;
+ /** @var FormFlash|null $flash */
+ $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line (form plugin)
+ $object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null;
}
}
@@ -139,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
* @param mixed $object
* @param int $time
* @return $this
+ * @throws JsonException
*/
public function setFlashCookieObject($name, $object, $time = 60)
{
- setcookie($name, json_encode($object), time() + $time, '/');
+ setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
return $this;
}
@@ -152,15 +163,28 @@ class Session extends \Grav\Framework\Session\Session
*
* @param string $name
* @return mixed|null
+ * @throws JsonException
*/
public function getFlashCookieObject($name)
{
if (isset($_COOKIE[$name])) {
- $object = json_decode($_COOKIE[$name]);
- setcookie($name, '', time() - 3600, '/');
- return $object;
+ $cookie = $_COOKIE[$name];
+ setcookie($name, '', $this->getCookieOptions(-42000));
+
+ return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
}
return null;
}
+
+ /**
+ * @return void
+ */
+ protected function onSessionStart(): void
+ {
+ $event = new SessionStartEvent($this);
+
+ $grav = Grav::instance();
+ $grav->dispatchEvent($event);
+ }
}
diff --git a/system/src/Grav/Common/Taxonomy.php b/system/src/Grav/Common/Taxonomy.php
index e3236472..0d0a450b 100644
--- a/system/src/Grav/Common/Taxonomy.php
+++ b/system/src/Grav/Common/Taxonomy.php
@@ -3,7 +3,7 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -12,6 +12,7 @@ namespace Grav\Common;
use Grav\Common\Config\Config;
use Grav\Common\Page\Collection;
use Grav\Common\Page\Interfaces\PageInterface;
+use function is_string;
/**
* The Taxonomy object is a singleton that holds a reference to a 'taxonomy map'. This map is
@@ -32,7 +33,9 @@ use Grav\Common\Page\Interfaces\PageInterface;
*/
class Taxonomy
{
+ /** @var array */
protected $taxonomy_map;
+ /** @var Grav */
protected $grav;
/**
@@ -51,28 +54,60 @@ class Taxonomy
* then adds those taxonomies to the map
*
* @param PageInterface $page the page to process
- * @param array $page_taxonomy
+ * @param array|null $page_taxonomy
*/
public function addTaxonomy(PageInterface $page, $page_taxonomy = null)
{
+ if (!$page->published()) {
+ return;
+ }
+
if (!$page_taxonomy) {
$page_taxonomy = $page->taxonomy();
}
- if (empty($page_taxonomy) || !$page->published()) {
+ if (empty($page_taxonomy)) {
return;
}
/** @var Config $config */
$config = $this->grav['config'];
- if ($config->get('site.taxonomies')) {
- foreach ((array)$config->get('site.taxonomies') as $taxonomy) {
- if (isset($page_taxonomy[$taxonomy])) {
- foreach ((array)$page_taxonomy[$taxonomy] as $item) {
- $this->taxonomy_map[$taxonomy][(string)$item][$page->path()] = ['slug' => $page->slug()];
- }
- }
+ $taxonomies = (array)$config->get('site.taxonomies');
+ foreach ($taxonomies as $taxonomy) {
+ // Skip invalid taxonomies.
+ if (!\is_string($taxonomy)) {
+ continue;
+ }
+ $current = $page_taxonomy[$taxonomy] ?? null;
+ foreach ((array)$current as $item) {
+ $this->iterateTaxonomy($page, $taxonomy, '', $item);
+ }
+ }
+ }
+
+ /**
+ * Iterate through taxonomy fields
+ *
+ * Reduces [taxonomy_type] to dot-notation where necessary
+ *
+ * @param PageInterface $page The Page to process
+ * @param string $taxonomy Taxonomy type to add
+ * @param string $key Taxonomy type to concatenate
+ * @param iterable|string $value Taxonomy value to add or iterate
+ * @return void
+ */
+ public function iterateTaxonomy(PageInterface $page, string $taxonomy, string $key, $value)
+ {
+ if (is_iterable($value)) {
+ foreach ($value as $identifier => $item) {
+ $identifier = "{$key}.{$identifier}";
+ $this->iterateTaxonomy($page, $taxonomy, $identifier, $item);
}
+ } elseif (is_string($value)) {
+ if (!empty($key)) {
+ $taxonomy .= $key;
+ }
+ $this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()];
}
}
@@ -82,7 +117,6 @@ class Taxonomy
*
* @param array $taxonomies taxonomies to search, eg ['tag'=>['animal','cat']]
* @param string $operator can be 'or' or 'and' (defaults to 'and')
- *
* @return Collection Collection object set to contain matches found in the taxonomy map
*/
public function findTaxonomy($taxonomies, $operator = 'and')
@@ -117,8 +151,7 @@ class Taxonomy
/**
* Gets and Sets the taxonomy map
*
- * @param array $var the taxonomy map
- *
+ * @param array|null $var the taxonomy map
* @return array the taxonomy map
*/
public function taxonomy($var = null)
@@ -134,7 +167,6 @@ class Taxonomy
* Gets item keys per taxonomy
*
* @param string $taxonomy taxonomy name
- *
* @return array keys of this taxonomy
*/
public function getTaxonomyItemKeys($taxonomy)
diff --git a/system/src/Grav/Common/Theme.php b/system/src/Grav/Common/Theme.php
index dbc589f0..a5006a21 100644
--- a/system/src/Grav/Common/Theme.php
+++ b/system/src/Grav/Common/Theme.php
@@ -3,16 +3,20 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
-use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Config\Config;
use RocketTheme\Toolbox\File\YamlFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+/**
+ * Class Theme
+ * @package Grav\Common
+ */
class Theme extends Plugin
{
/**
@@ -30,67 +34,54 @@ class Theme extends Plugin
/**
* Get configuration of the plugin.
*
- * @return Config
+ * @return array
*/
public function config()
{
- return $this->config["themes.{$this->name}"];
+ return $this->config["themes.{$this->name}"] ?? [];
}
/**
* Persists to disk the theme parameters currently stored in the Grav Config object
*
- * @param string $theme_name The name of the theme whose config it should store.
- *
- * @return true
+ * @param string $name The name of the theme whose config it should store.
+ * @return bool
*/
- public static function saveConfig($theme_name)
+ public static function saveConfig($name)
{
- if (!$theme_name) {
+ if (!$name) {
return false;
}
$grav = Grav::instance();
+
+ /** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $filename = 'config://themes/' . $theme_name . '.yaml';
- $file = YamlFile::instance($locator->findResource($filename, true, true));
- $content = $grav['config']->get('themes.' . $theme_name);
+
+ $filename = 'config://themes/' . $name . '.yaml';
+ $file = YamlFile::instance((string)$locator->findResource($filename, true, true));
+ $content = $grav['config']->get('themes.' . $name);
$file->save($content);
$file->free();
+ unset($file);
return true;
}
- /**
- * Override the mergeConfig method to work for themes
- */
- protected function mergeConfig(PageInterface $page, $deep = 'merge', $params = [], $type = 'themes')
- {
- return parent::mergeConfig($page, $deep, $params, $type);
- }
-
- /**
- * Simpler getter for the theme blueprint
- *
- * @return mixed
- */
- public function getBlueprint()
- {
- if (!$this->blueprint) {
- $this->loadBlueprint();
- }
- return $this->blueprint;
- }
-
/**
* Load blueprints.
+ *
+ * @return void
*/
protected function loadBlueprint()
{
if (!$this->blueprint) {
$grav = Grav::instance();
+ /** @var Themes $themes */
$themes = $grav['themes'];
- $this->blueprint = $themes->get($this->name)->blueprints();
+ $data = $themes->get($this->name);
+ \assert($data !== null);
+ $this->blueprint = $data->blueprints();
}
}
}
diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php
index 6720d462..6126841c 100644
--- a/system/src/Grav/Common/Themes.php
+++ b/system/src/Grav/Common/Themes.php
@@ -3,28 +3,39 @@
/**
* @package Grav\Common
*
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
+use DirectoryIterator;
+use Exception;
use Grav\Common\Config\Config;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Data\Blueprints;
use Grav\Common\Data\Data;
-use RocketTheme\Toolbox\Event\EventDispatcher;
-use RocketTheme\Toolbox\Event\EventSubscriberInterface;
+use Grav\Framework\Psr7\Response;
+use InvalidArgumentException;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RuntimeException;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use function defined;
+use function in_array;
+use function strlen;
+/**
+ * Class Themes
+ * @package Grav\Common
+ */
class Themes extends Iterator
{
/** @var Grav */
protected $grav;
-
/** @var Config */
protected $config;
-
+ /** @var bool */
protected $inited = false;
/**
@@ -43,6 +54,9 @@ class Themes extends Iterator
spl_autoload_register([$this, 'autoloadTheme']);
}
+ /**
+ * @return void
+ */
public function init()
{
/** @var Themes $themes */
@@ -52,6 +66,9 @@ class Themes extends Iterator
$this->initTheme();
}
+ /**
+ * @return void
+ */
public function initTheme()
{
if ($this->inited === false) {
@@ -60,17 +77,36 @@ class Themes extends Iterator
try {
$instance = $themes->load();
- } catch (\InvalidArgumentException $e) {
- throw new \RuntimeException($this->current() . ' theme could not be found');
+ } catch (InvalidArgumentException $e) {
+ throw new RuntimeException($this->current() . ' theme could not be found');
}
+ // Register autoloader.
+ if (method_exists($instance, 'autoload')) {
+ $instance->autoload();
+ }
+
+ // Register event listeners.
if ($instance instanceof EventSubscriberInterface) {
/** @var EventDispatcher $events */
$events = $this->grav['events'];
-
$events->addSubscriber($instance);
}
+ // Register blueprints.
+ if (is_dir('theme://blueprints/pages')) {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->grav['locator'];
+ $locator->addPath('blueprints', '', ['theme://blueprints'], ['user', 'blueprints']);
+ }
+
+ // Register form fields.
+ if (method_exists($instance, 'getFormFieldTypes')) {
+ /** @var Plugins $plugins */
+ $plugins = $this->grav['plugins'];
+ $plugins->formFieldTypes = $instance->getFormFieldTypes() + $plugins->formFieldTypes;
+ }
+
$this->grav['theme'] = $instance;
$this->grav->fireEvent('onThemeInitialized');
@@ -93,7 +129,7 @@ class Themes extends Iterator
$iterator = $locator->getIterator('themes://');
- /** @var \DirectoryIterator $directory */
+ /** @var DirectoryIterator $directory */
foreach ($iterator as $directory) {
if (!$directory->isDir() || $directory->isDot()) {
continue;
@@ -103,8 +139,8 @@ class Themes extends Iterator
try {
$result = $this->get($theme);
- } catch (\Exception $e) {
- $exception = new \RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e);
+ } catch (Exception $e) {
+ $exception = new RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e);
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
@@ -118,7 +154,7 @@ class Themes extends Iterator
$list[$theme] = $result;
}
}
- ksort($list);
+ ksort($list, SORT_NATURAL | SORT_FLAG_CASE);
return $list;
}
@@ -127,14 +163,13 @@ class Themes extends Iterator
* Get theme configuration or throw exception if it cannot be found.
*
* @param string $name
- *
- * @return Data
- * @throws \RuntimeException
+ * @return Data|null
+ * @throws RuntimeException
*/
public function get($name)
{
if (!$name) {
- throw new \RuntimeException('Theme name not provided.');
+ throw new RuntimeException('Theme name not provided.');
}
$blueprints = new Blueprints('themes://');
@@ -189,47 +224,55 @@ class Themes extends Iterator
$grav = $this->grav;
$config = $this->config;
$name = $this->current();
+ $class = null;
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
- $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
-
- $inflector = $grav['inflector'];
+ // Start by attempting to load the theme.php file.
+ $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
if ($file) {
// Local variables available in the file: $grav, $config, $name, $file
$class = include $file;
+ if (!\is_object($class) || !is_subclass_of($class, Theme::class, true)) {
+ $class = null;
+ }
+ } elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
+ $response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
- if (!is_object($class)) {
- $themeClassFormat = [
- 'Grav\\Theme\\' . ucfirst($name),
- 'Grav\\Theme\\' . $inflector->camelize($name)
- ];
+ $grav->close($response);
+ }
- foreach ($themeClassFormat as $themeClass) {
- if (class_exists($themeClass)) {
- $class = new $themeClass($grav, $config, $name);
- break;
- }
+ // If the class hasn't been initialized yet, guess the class name and create a new instance.
+ if (null === $class) {
+ $themeClassFormat = [
+ 'Grav\\Theme\\' . Inflector::camelize($name),
+ 'Grav\\Theme\\' . ucfirst($name)
+ ];
+
+ foreach ($themeClassFormat as $themeClass) {
+ if (is_subclass_of($themeClass, Theme::class, true)) {
+ $class = new $themeClass($grav, $config, $name);
+ break;
}
}
- } elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
- exit("Theme '$name' does not exist, unable to display page.");
}
- $this->config->set('theme', $config->get('themes.' . $name));
-
- if (empty($class)) {
+ // Finally if everything else fails, just create a new instance from the default Theme class.
+ if (null === $class) {
$class = new Theme($grav, $config, $name);
}
+ $this->config->set('theme', $config->get('themes.' . $name));
+
return $class;
}
/**
* Configure and prepare streams for current template.
*
- * @throws \InvalidArgumentException
+ * @return void
+ * @throws InvalidArgumentException
*/
public function configure()
{
@@ -261,7 +304,7 @@ class Themes extends Iterator
}
}
- if (\in_array($scheme, $registered, true)) {
+ if (in_array($scheme, $registered, true)) {
stream_wrapper_unregister($scheme);
}
$type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
@@ -270,7 +313,7 @@ class Themes extends Iterator
}
if (!stream_wrapper_register($scheme, $type)) {
- throw new \InvalidArgumentException("Stream '{$type}' could not be initialized.");
+ throw new InvalidArgumentException("Stream '{$type}' could not be initialized.");
}
}
@@ -283,6 +326,7 @@ class Themes extends Iterator
*
* @param string $name Theme name
* @param Config $config Configuration class
+ * @return void
*/
protected function loadConfiguration($name, Config $config)
{
@@ -292,8 +336,10 @@ class Themes extends Iterator
/**
* Load theme languages.
+ * Reads ALL language files from theme stream and merges them.
*
* @param Config $config Configuration class
+ * @return void
*/
protected function loadLanguages(Config $config)
{
@@ -301,17 +347,15 @@ class Themes extends Iterator
$locator = $this->grav['locator'];
if ($config->get('system.languages.translations', true)) {
- $language_file = $locator->findResource('theme://languages' . YAML_EXT);
- if ($language_file) {
+ $language_files = array_reverse($locator->findResources('theme://languages' . YAML_EXT));
+ foreach ($language_files as $language_file) {
$language = CompiledYamlFile::instance($language_file)->content();
$this->grav['languages']->mergeRecursive($language);
}
- $languages_folder = $locator->findResource('theme://languages');
- if (file_exists($languages_folder)) {
+ $languages_folders = array_reverse($locator->findResources('theme://languages'));
+ foreach ($languages_folders as $languages_folder) {
$languages = [];
- $iterator = new \DirectoryIterator($languages_folder);
-
- /** @var \DirectoryIterator $directory */
+ $iterator = new DirectoryIterator($languages_folder);
foreach ($iterator as $file) {
if ($file->getExtension() !== 'yaml') {
continue;
@@ -327,8 +371,7 @@ class Themes extends Iterator
* Autoload theme classes for inheritance
*
* @param string $class Class name
- *
- * @return mixed false FALSE if unable to load $class; Class name if
+ * @return mixed|false FALSE if unable to load $class; Class name if
* $class is successfully loaded
*/
protected function autoloadTheme($class)
@@ -357,7 +400,10 @@ class Themes extends Iterator
}
// Try Old style theme classes
- $path = strtolower(preg_replace('#\\\|_(?!.+\\\)#', '/', $class));
+ $path = preg_replace('#\\\|_(?!.+\\\)#', '/', $class);
+ \assert(null !== $path);
+
+ $path = strtolower($path);
$file = $locator("themes://{$path}/theme.php") ?: $locator("themes://{$path}/{$path}.php");
// Load class
diff --git a/system/src/Grav/Common/Twig/Exception/TwigException.php b/system/src/Grav/Common/Twig/Exception/TwigException.php
new file mode 100644
index 00000000..8f543dcc
--- /dev/null
+++ b/system/src/Grav/Common/Twig/Exception/TwigException.php
@@ -0,0 +1,19 @@
+locator = Grav::instance()['locator'];
+ }
+
+ /**
+ * @return TwigFilter[]
+ */
+ public function getFilters()
+ {
+ return [
+ new TwigFilter('file_exists', [$this, 'file_exists']),
+ new TwigFilter('fileatime', [$this, 'fileatime']),
+ new TwigFilter('filectime', [$this, 'filectime']),
+ new TwigFilter('filemtime', [$this, 'filemtime']),
+ new TwigFilter('filesize', [$this, 'filesize']),
+ new TwigFilter('filetype', [$this, 'filetype']),
+ new TwigFilter('is_dir', [$this, 'is_dir']),
+ new TwigFilter('is_file', [$this, 'is_file']),
+ new TwigFilter('is_link', [$this, 'is_link']),
+ new TwigFilter('is_readable', [$this, 'is_readable']),
+ new TwigFilter('is_writable', [$this, 'is_writable']),
+ new TwigFilter('is_writeable', [$this, 'is_writable']),
+ new TwigFilter('lstat', [$this, 'lstat']),
+ new TwigFilter('getimagesize', [$this, 'getimagesize']),
+ new TwigFilter('exif_read_data', [$this, 'exif_read_data']),
+ new TwigFilter('read_exif_data', [$this, 'exif_read_data']),
+ new TwigFilter('exif_imagetype', [$this, 'exif_imagetype']),
+ new TwigFilter('hash_file', [$this, 'hash_file']),
+ new TwigFilter('hash_hmac_file', [$this, 'hash_hmac_file']),
+ new TwigFilter('md5_file', [$this, 'md5_file']),
+ new TwigFilter('sha1_file', [$this, 'sha1_file']),
+ new TwigFilter('get_meta_tags', [$this, 'get_meta_tags']),
+ new TwigFilter('pathinfo', [$this, 'pathinfo']),
+ ];
+ }
+
+ /**
+ * Return a list of all functions.
+ *
+ * @return TwigFunction[]
+ */
+ public function getFunctions()
+ {
+ return [
+ new TwigFunction('file_exists', [$this, 'file_exists']),
+ new TwigFunction('fileatime', [$this, 'fileatime']),
+ new TwigFunction('filectime', [$this, 'filectime']),
+ new TwigFunction('filemtime', [$this, 'filemtime']),
+ new TwigFunction('filesize', [$this, 'filesize']),
+ new TwigFunction('filetype', [$this, 'filetype']),
+ new TwigFunction('is_dir', [$this, 'is_dir']),
+ new TwigFunction('is_file', [$this, 'is_file']),
+ new TwigFunction('is_link', [$this, 'is_link']),
+ new TwigFunction('is_readable', [$this, 'is_readable']),
+ new TwigFunction('is_writable', [$this, 'is_writable']),
+ new TwigFunction('is_writeable', [$this, 'is_writable']),
+ new TwigFunction('lstat', [$this, 'lstat']),
+ new TwigFunction('getimagesize', [$this, 'getimagesize']),
+ new TwigFunction('exif_read_data', [$this, 'exif_read_data']),
+ new TwigFunction('read_exif_data', [$this, 'exif_read_data']),
+ new TwigFunction('exif_imagetype', [$this, 'exif_imagetype']),
+ new TwigFunction('hash_file', [$this, 'hash_file']),
+ new TwigFunction('hash_hmac_file', [$this, 'hash_hmac_file']),
+ new TwigFunction('md5_file', [$this, 'md5_file']),
+ new TwigFunction('sha1_file', [$this, 'sha1_file']),
+ new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']),
+ new TwigFunction('pathinfo', [$this, 'pathinfo']),
+ ];
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function file_exists($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return file_exists($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return int|false
+ */
+ public function fileatime($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return fileatime($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return int|false
+ */
+ public function filectime($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return filectime($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return int|false
+ */
+ public function filemtime($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return filemtime($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return int|false
+ */
+ public function filesize($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return filesize($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return string|false
+ */
+ public function filetype($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return filetype($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function is_dir($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return is_dir($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function is_file($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return is_file($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function is_link($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return is_link($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function is_readable($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return is_readable($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ public function is_writable($filename): bool
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return is_writable($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return array|false
+ */
+ public function lstat($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return lstat($filename);
+ }
+
+ /**
+ * @param string $filename
+ * @return array|false
+ */
+ public function getimagesize($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return getimagesize($filename);
+ }
+
+ /**
+ * @param string $file
+ * @param string|null $required_sections
+ * @param bool $as_arrays
+ * @param bool $read_thumbnail
+ * @return array|false
+ */
+ public function exif_read_data($file, ?string $required_sections, bool $as_arrays = false, bool $read_thumbnail = false)
+ {
+ if (!Utils::functionExists('exif_read_data') || !$this->checkFilename($file)) {
+ return false;
+ }
+
+ return exif_read_data($file, $required_sections, $as_arrays, $read_thumbnail);
+ }
+
+ /**
+ * @param string $filename
+ * @return string|false
+ */
+ public function exif_imagetype($filename)
+ {
+ if (!Utils::functionExists('exif_imagetype') || !$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return @exif_imagetype($filename);
+ }
+
+ /**
+ * @param string $algo
+ * @param string $filename
+ * @param bool $binary
+ * @return string|false
+ */
+ public function hash_file(string $algo, string $filename, bool $binary = false)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return hash_file($algo, $filename, $binary);
+ }
+
+ /**
+ * @param string $algo
+ * @param string $data
+ * @param string $key
+ * @param bool $binary
+ * @return string|false
+ */
+ public function hash_hmac_file(string $algo, string $data, string $key, bool $binary = false)
+ {
+ if (!$this->checkFilename($data)) {
+ return false;
+ }
+
+ return hash_hmac_file($algo, $data, $key, $binary);
+ }
+
+ /**
+ * @param string $filename
+ * @param bool $binary
+ * @return string|false
+ */
+ public function md5_file($filename, bool $binary = false)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return md5_file($filename, $binary);
+ }
+
+ /**
+ * @param string $filename
+ * @param bool $binary
+ * @return string|false
+ */
+ public function sha1_file($filename, bool $binary = false)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return sha1_file($filename, $binary);
+ }
+
+ /**
+ * @param string $filename
+ * @return array|false
+ */
+ public function get_meta_tags($filename)
+ {
+ if (!$this->checkFilename($filename)) {
+ return false;
+ }
+
+ return get_meta_tags($filename);
+ }
+
+ /**
+ * @param string $path
+ * @param int|null $flags
+ * @return string|string[]
+ */
+ public function pathinfo($path, $flags = null)
+ {
+ if (null !== $flags) {
+ return pathinfo($path, (int)$flags);
+ }
+
+ return pathinfo($path);
+ }
+
+ /**
+ * @param string $filename
+ * @return bool
+ */
+ private function checkFilename($filename): bool
+ {
+ return is_string($filename) && (!str_contains($filename, '://') || $this->locator->isStream($filename));
+ }
+}
diff --git a/system/src/Grav/Common/Twig/Extension/GravExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php
new file mode 100644
index 00000000..638617c4
--- /dev/null
+++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php
@@ -0,0 +1,1635 @@
+grav = Grav::instance();
+ $this->debugger = $this->grav['debugger'] ?? null;
+ $this->config = $this->grav['config'];
+ }
+
+ /**
+ * Register some standard globals
+ *
+ * @return array
+ */
+ public function getGlobals(): array
+ {
+ return [
+ 'grav' => $this->grav,
+ ];
+ }
+
+ /**
+ * Return a list of all filters.
+ *
+ * @return array
+ */
+ public function getFilters(): array
+ {
+ return [
+ new TwigFilter('*ize', [$this, 'inflectorFilter']),
+ new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']),
+ new TwigFilter('contains', [$this, 'containsFilter']),
+ new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']),
+ new TwigFilter('nicenumber', [$this, 'niceNumberFunc']),
+ new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']),
+ new TwigFilter('nicetime', [$this, 'nicetimeFunc']),
+ new TwigFilter('defined', [$this, 'definedDefaultFilter']),
+ new TwigFilter('ends_with', [$this, 'endsWithFilter']),
+ new TwigFilter('fieldName', [$this, 'fieldNameFilter']),
+ new TwigFilter('ksort', [$this, 'ksortFilter']),
+ new TwigFilter('ltrim', [$this, 'ltrimFilter']),
+ new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
+ new TwigFilter('md5', [$this, 'md5Filter']),
+ new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']),
+ new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']),
+ new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']),
+ new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']),
+ new TwigFilter('randomize', [$this, 'randomizeFilter']),
+ new TwigFilter('modulus', [$this, 'modulusFilter']),
+ new TwigFilter('rtrim', [$this, 'rtrimFilter']),
+ new TwigFilter('pad', [$this, 'padFilter']),
+ new TwigFilter('regex_replace', [$this, 'regexReplace']),
+ new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]),
+ new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']),
+ new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']),
+ new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']),
+ new TwigFilter('starts_with', [$this, 'startsWithFilter']),
+ new TwigFilter('truncate', [Utils::class, 'truncate']),
+ new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']),
+ new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']),
+ new TwigFilter('array_unique', 'array_unique'),
+ new TwigFilter('basename', 'basename'),
+ new TwigFilter('dirname', 'dirname'),
+ new TwigFilter('print_r', [$this, 'print_r']),
+ new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
+ new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
+ new TwigFilter('nicecron', [$this, 'niceCronFilter']),
+
+ // Translations
+ new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]),
+ new TwigFilter('tl', [$this, 'translateLanguage']),
+ new TwigFilter('ta', [$this, 'translateArray']),
+
+ // Casting values
+ new TwigFilter('string', [$this, 'stringFilter']),
+ new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]),
+ new TwigFilter('bool', [$this, 'boolFilter']),
+ new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
+ new TwigFilter('array', [$this, 'arrayFilter']),
+ new TwigFilter('yaml', [$this, 'yamlFilter']),
+
+ // Object Types
+ new TwigFilter('get_type', [$this, 'getTypeFunc']),
+ new TwigFilter('of_type', [$this, 'ofTypeFunc']),
+
+ // PHP methods
+ new TwigFilter('count', 'count'),
+ new TwigFilter('array_diff', 'array_diff'),
+ ];
+ }
+
+ /**
+ * Return a list of all functions.
+ *
+ * @return array
+ */
+ public function getFunctions(): array
+ {
+ return [
+ new TwigFunction('array', [$this, 'arrayFilter']),
+ new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']),
+ new TwigFunction('array_key_exists', 'array_key_exists'),
+ new TwigFunction('array_unique', 'array_unique'),
+ new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']),
+ new TwigFunction('array_diff', 'array_diff'),
+ new TwigFunction('authorize', [$this, 'authorize']),
+ new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
+ new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
+ new TwigFunction('vardump', [$this, 'vardumpFunc']),
+ new TwigFunction('print_r', [$this, 'print_r']),
+ new TwigFunction('http_response_code', 'http_response_code'),
+ new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]),
+ new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]),
+ new TwigFunction('gist', [$this, 'gistFunc']),
+ new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']),
+ new TwigFunction('pathinfo', 'pathinfo'),
+ new TwigFunction('random_string', [$this, 'randomStringFunc']),
+ new TwigFunction('repeat', [$this, 'repeatFunc']),
+ new TwigFunction('regex_replace', [$this, 'regexReplace']),
+ new TwigFunction('regex_filter', [$this, 'regexFilter']),
+ new TwigFunction('regex_match', [$this, 'regexMatch']),
+ new TwigFunction('regex_split', [$this, 'regexSplit']),
+ new TwigFunction('string', [$this, 'stringFilter']),
+ new TwigFunction('url', [$this, 'urlFunc']),
+ new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']),
+ new TwigFunction('get_cookie', [$this, 'getCookie']),
+ new TwigFunction('redirect_me', [$this, 'redirectFunc']),
+ new TwigFunction('range', [$this, 'rangeFunc']),
+ new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']),
+ new TwigFunction('exif', [$this, 'exifFunc']),
+ new TwigFunction('media_directory', [$this, 'mediaDirFunc']),
+ new TwigFunction('body_class', [$this, 'bodyClassFunc'], ['needs_context' => true]),
+ new TwigFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]),
+ new TwigFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]),
+ new TwigFunction('read_file', [$this, 'readFileFunc']),
+ new TwigFunction('nicenumber', [$this, 'niceNumberFunc']),
+ new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
+ new TwigFunction('nicetime', [$this, 'nicetimeFunc']),
+ new TwigFunction('cron', [$this, 'cronFunc']),
+ new TwigFunction('svg_image', [$this, 'svgImageFunction']),
+ new TwigFunction('xss', [$this, 'xssFunc']),
+ new TwigFunction('unique_id', [$this, 'uniqueId']),
+
+ // Translations
+ new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
+ new TwigFunction('tl', [$this, 'translateLanguage']),
+ new TwigFunction('ta', [$this, 'translateArray']),
+
+ // Object Types
+ new TwigFunction('get_type', [$this, 'getTypeFunc']),
+ new TwigFunction('of_type', [$this, 'ofTypeFunc']),
+
+ // PHP methods
+ new TwigFunction('is_numeric', 'is_numeric'),
+ new TwigFunction('is_iterable', 'is_iterable'),
+ new TwigFunction('is_countable', 'is_countable'),
+ new TwigFunction('is_null', 'is_null'),
+ new TwigFunction('is_string', 'is_string'),
+ new TwigFunction('is_array', 'is_array'),
+ new TwigFunction('is_object', 'is_object'),
+ new TwigFunction('count', 'count'),
+ new TwigFunction('array_diff', 'array_diff'),
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function getTokenParsers(): array
+ {
+ return [
+ new TwigTokenParserRender(),
+ new TwigTokenParserThrow(),
+ new TwigTokenParserTryCatch(),
+ new TwigTokenParserScript(),
+ new TwigTokenParserStyle(),
+ new TwigTokenParserMarkdown(),
+ new TwigTokenParserSwitch(),
+ new TwigTokenParserCache(),
+ ];
+ }
+
+ public function print_r($var)
+ {
+ return print_r($var, true);
+ }
+
+ /**
+ * Filters field name by changing dot notation into array notation.
+ *
+ * @param string $str
+ * @return string
+ */
+ public function fieldNameFilter($str)
+ {
+ $path = explode('.', rtrim($str, '.'));
+
+ return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
+ }
+
+ /**
+ * Protects email address.
+ *
+ * @param string $str
+ * @return string
+ */
+ public function safeEmailFilter($str)
+ {
+ static $list = [
+ '"' => '"',
+ "'" => ''',
+ '&' => '&',
+ '<' => '<',
+ '>' => '>',
+ '@' => '@'
+ ];
+
+ $characters = mb_str_split($str, 1, 'UTF-8');
+
+ $encoded = '';
+ foreach ($characters as $chr) {
+ $encoded .= $list[$chr] ?? (random_int(0, 1) ? '' . mb_ord($chr) . ';' : $chr);
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * Returns array in a random order.
+ *
+ * @param array|Traversable $original
+ * @param int $offset Can be used to return only slice of the array.
+ * @return array
+ */
+ public function randomizeFilter($original, $offset = 0)
+ {
+ if ($original instanceof Traversable) {
+ $original = iterator_to_array($original, false);
+ }
+
+ if (!is_array($original)) {
+ return $original;
+ }
+
+ $sorted = [];
+ $random = array_slice($original, $offset);
+ shuffle($random);
+
+ $sizeOf = count($original);
+ for ($x = 0; $x < $sizeOf; $x++) {
+ if ($x < $offset) {
+ $sorted[] = $original[$x];
+ } else {
+ $sorted[] = array_shift($random);
+ }
+ }
+
+ return $sorted;
+ }
+
+ /**
+ * Returns the modulus of an integer
+ *
+ * @param string|int $number
+ * @param int $divider
+ * @param array|null $items array of items to select from to return
+ * @return int
+ */
+ public function modulusFilter($number, $divider, $items = null)
+ {
+ if (is_string($number)) {
+ $number = strlen($number);
+ }
+
+ $remainder = $number % $divider;
+
+ if (is_array($items)) {
+ return $items[$remainder] ?? $items[0];
+ }
+
+ return $remainder;
+ }
+
+ /**
+ * Inflector supports following notations:
+ *
+ * `{{ 'person'|pluralize }} => people`
+ * `{{ 'shoes'|singularize }} => shoe`
+ * `{{ 'welcome page'|titleize }} => "Welcome Page"`
+ * `{{ 'send_email'|camelize }} => SendEmail`
+ * `{{ 'CamelCased'|underscorize }} => camel_cased`
+ * `{{ 'Something Text'|hyphenize }} => something-text`
+ * `{{ 'something_text_to_read'|humanize }} => "Something text to read"`
+ * `{{ '181'|monthize }} => 5`
+ * `{{ '10'|ordinalize }} => 10th`
+ *
+ * @param string $action
+ * @param string $data
+ * @param int|null $count
+ * @return string
+ */
+ public function inflectorFilter($action, $data, $count = null)
+ {
+ $action .= 'ize';
+
+ /** @var Inflector $inflector */
+ $inflector = $this->grav['inflector'];
+
+ if (in_array(
+ $action,
+ ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'],
+ true
+ )) {
+ return $inflector->{$action}($data);
+ }
+
+ if (in_array($action, ['pluralize', 'singularize'], true)) {
+ return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Return MD5 hash from the input.
+ *
+ * @param string $str
+ * @return string
+ */
+ public function md5Filter($str)
+ {
+ return md5($str);
+ }
+
+ /**
+ * Return Base32 encoded string
+ *
+ * @param string $str
+ * @return string
+ */
+ public function base32EncodeFilter($str)
+ {
+ return Base32::encode($str);
+ }
+
+ /**
+ * Return Base32 decoded string
+ *
+ * @param string $str
+ * @return string
+ */
+ public function base32DecodeFilter($str)
+ {
+ return Base32::decode($str);
+ }
+
+ /**
+ * Return Base64 encoded string
+ *
+ * @param string $str
+ * @return string
+ */
+ public function base64EncodeFilter($str)
+ {
+ return base64_encode($str);
+ }
+
+ /**
+ * Return Base64 decoded string
+ *
+ * @param string $str
+ * @return string|false
+ */
+ public function base64DecodeFilter($str)
+ {
+ return base64_decode($str);
+ }
+
+ /**
+ * Sorts a collection by key
+ *
+ * @param array $input
+ * @param string $filter
+ * @param int $direction
+ * @param int $sort_flags
+ * @return array
+ */
+ public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR)
+ {
+ return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags);
+ }
+
+ /**
+ * Return ksorted collection.
+ *
+ * @param array|null $array
+ * @return array
+ */
+ public function ksortFilter($array)
+ {
+ if (null === $array) {
+ $array = [];
+ }
+ ksort($array);
+
+ return $array;
+ }
+
+ /**
+ * Wrapper for chunk_split() function
+ *
+ * @param string $value
+ * @param int $chars
+ * @param string $split
+ * @return string
+ */
+ public function chunkSplitFilter($value, $chars, $split = '-')
+ {
+ return chunk_split($value, $chars, $split);
+ }
+
+ /**
+ * determine if a string contains another
+ *
+ * @param string $haystack
+ * @param string $needle
+ * @return string|bool
+ * @todo returning $haystack here doesn't make much sense
+ */
+ public function containsFilter($haystack, $needle)
+ {
+ if (empty($needle)) {
+ return $haystack;
+ }
+
+ return (strpos($haystack, (string) $needle) !== false);
+ }
+
+ /**
+ * Gets a human readable output for cron syntax
+ *
+ * @param string $at
+ * @return string
+ */
+ public function niceCronFilter($at)
+ {
+ $cron = new Cron($at);
+ return $cron->getText('en');
+ }
+
+ /**
+ * Get Cron object for a crontab 'at' format
+ *
+ * @param string $at
+ * @return CronExpression
+ */
+ public function cronFunc($at)
+ {
+ return CronExpression::factory($at);
+ }
+
+ /**
+ * displays a facebook style 'time ago' formatted date/time
+ *
+ * @param string $date
+ * @param bool $long_strings
+ * @param bool $show_tense
+ * @return string
+ */
+ public function nicetimeFunc($date, $long_strings = true, $show_tense = true)
+ {
+ if (empty($date)) {
+ return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED');
+ }
+
+ if ($long_strings) {
+ $periods = [
+ 'NICETIME.SECOND',
+ 'NICETIME.MINUTE',
+ 'NICETIME.HOUR',
+ 'NICETIME.DAY',
+ 'NICETIME.WEEK',
+ 'NICETIME.MONTH',
+ 'NICETIME.YEAR',
+ 'NICETIME.DECADE'
+ ];
+ } else {
+ $periods = [
+ 'NICETIME.SEC',
+ 'NICETIME.MIN',
+ 'NICETIME.HR',
+ 'NICETIME.DAY',
+ 'NICETIME.WK',
+ 'NICETIME.MO',
+ 'NICETIME.YR',
+ 'NICETIME.DEC'
+ ];
+ }
+
+ $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
+
+ $now = time();
+
+ // check if unix timestamp
+ if ((string)(int)$date === (string)$date) {
+ $unix_date = $date;
+ } else {
+ $unix_date = strtotime($date);
+ }
+
+ // check validity of date
+ if (empty($unix_date)) {
+ return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE');
+ }
+
+ // is it future date or past date
+ if ($now > $unix_date) {
+ $difference = $now - $unix_date;
+ $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO');
+ } elseif ($now == $unix_date) {
+ $difference = $now - $unix_date;
+ $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW');
+ } else {
+ $difference = $unix_date - $now;
+ $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW');
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ if ($difference != 1) {
+ $periods[$j] .= '_PLURAL';
+ }
+
+ if ($this->grav['language']->getTranslation(
+ $this->grav['language']->getLanguage(),
+ $periods[$j] . '_MORE_THAN_TWO'
+ )
+ ) {
+ if ($difference > 2) {
+ $periods[$j] .= '_MORE_THAN_TWO';
+ }
+ }
+
+ $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]);
+
+ if ($now == $unix_date) {
+ return $tense;
+ }
+
+ $time = "{$difference} {$periods[$j]}";
+ $time .= $show_tense ? " {$tense}" : '';
+
+ return $time;
+ }
+
+ /**
+ * Allow quick check of a string for XSS Vulnerabilities
+ *
+ * @param string|array $data
+ * @return bool|string|array
+ */
+ public function xssFunc($data)
+ {
+ if (!is_array($data)) {
+ return Security::detectXss($data);
+ }
+
+ $results = Security::detectXssFromArray($data);
+ $results_parts = array_map(static function ($value, $key) {
+ return $key.': \''.$value . '\'';
+ }, array_values($results), array_keys($results));
+
+ return implode(', ', $results_parts);
+ }
+
+ /**
+ * Generates a random string with configurable length, prefix and suffix.
+ * Unlike the built-in `uniqid()`, this string is non-conflicting and safe
+ *
+ * @param int $length
+ * @param array $options
+ * @return string
+ * @throws \Exception
+ */
+ public function uniqueId(int $length = 9, array $options = ['prefix' => '', 'suffix' => '']): string
+ {
+ return Utils::uniqueId($length, $options);
+ }
+
+ /**
+ * @param string $string
+ * @return string
+ */
+ public function absoluteUrlFilter($string)
+ {
+ $url = $this->grav['uri']->base();
+ $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string);
+
+ return $string;
+ }
+
+ /**
+ * @param array $context
+ * @param string $string
+ * @param bool $block Block or Line processing
+ * @return string
+ */
+ public function markdownFunction($context, $string, $block = true)
+ {
+ $page = $context['page'] ?? null;
+ return Utils::processMarkdown($string, $block, $page);
+ }
+
+ /**
+ * @param string $haystack
+ * @param string $needle
+ * @return bool
+ */
+ public function startsWithFilter($haystack, $needle)
+ {
+ return Utils::startsWith($haystack, $needle);
+ }
+
+ /**
+ * @param string $haystack
+ * @param string $needle
+ * @return bool
+ */
+ public function endsWithFilter($haystack, $needle)
+ {
+ return Utils::endsWith($haystack, $needle);
+ }
+
+ /**
+ * @param mixed $value
+ * @param null $default
+ * @return mixed|null
+ */
+ public function definedDefaultFilter($value, $default = null)
+ {
+ return $value ?? $default;
+ }
+
+ /**
+ * @param string $value
+ * @param string|null $chars
+ * @return string
+ */
+ public function rtrimFilter($value, $chars = null)
+ {
+ return null !== $chars ? rtrim($value, $chars) : rtrim($value);
+ }
+
+ /**
+ * @param string $value
+ * @param string|null $chars
+ * @return string
+ */
+ public function ltrimFilter($value, $chars = null)
+ {
+ return null !== $chars ? ltrim($value, $chars) : ltrim($value);
+ }
+
+ /**
+ * Returns a string from a value. If the value is array, return it json encoded
+ *
+ * @param mixed $value
+ * @return string
+ */
+ public function stringFilter($value)
+ {
+ // Format the array as a string
+ if (is_array($value)) {
+ return json_encode($value);
+ }
+
+ // Boolean becomes '1' or '0'
+ if (is_bool($value)) {
+ $value = (int)$value;
+ }
+
+ // Cast the other values to string.
+ return (string)$value;
+ }
+
+ /**
+ * Casts input to int.
+ *
+ * @param mixed $input
+ * @return int
+ */
+ public function intFilter($input)
+ {
+ return (int) $input;
+ }
+
+ /**
+ * Casts input to bool.
+ *
+ * @param mixed $input
+ * @return bool
+ */
+ public function boolFilter($input)
+ {
+ return (bool) $input;
+ }
+
+ /**
+ * Casts input to float.
+ *
+ * @param mixed $input
+ * @return float
+ */
+ public function floatFilter($input)
+ {
+ return (float) $input;
+ }
+
+ /**
+ * Casts input to array.
+ *
+ * @param mixed $input
+ * @return array
+ */
+ public function arrayFilter($input)
+ {
+ if (is_array($input)) {
+ return $input;
+ }
+
+ if (is_object($input)) {
+ if (method_exists($input, 'toArray')) {
+ return $input->toArray();
+ }
+
+ if ($input instanceof Iterator) {
+ return iterator_to_array($input);
+ }
+ }
+
+ return (array)$input;
+ }
+
+ /**
+ * @param array|object $value
+ * @param int|null $inline
+ * @param int|null $indent
+ * @return string
+ */
+ public function yamlFilter($value, $inline = null, $indent = null): string
+ {
+ return Yaml::dump($value, $inline, $indent);
+ }
+
+ /**
+ * @param Environment $twig
+ * @return string
+ */
+ public function translate(Environment $twig, ...$args)
+ {
+ // If admin and tu filter provided, use it
+ if (isset($this->grav['admin'])) {
+ $numargs = count($args);
+ $lang = null;
+
+ if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) {
+ $lang = array_pop($args);
+ } elseif ($numargs === 2 && is_array($args[1])) {
+ $subs = array_pop($args);
+ $args = array_merge($args, $subs);
+ }
+
+ return $this->grav['admin']->translate($args, $lang);
+ }
+
+ // else use the default grav translate functionality
+ return $this->grav['language']->translate($args);
+ }
+
+ /**
+ * Translate Strings
+ *
+ * @param string|array $args
+ * @param array|null $languages
+ * @param bool $array_support
+ * @param bool $html_out
+ * @return string
+ */
+ public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false)
+ {
+ /** @var Language $language */
+ $language = $this->grav['language'];
+
+ return $language->translate($args, $languages, $array_support, $html_out);
+ }
+
+ /**
+ * @param string $key
+ * @param string $index
+ * @param array|null $lang
+ * @return string
+ */
+ public function translateArray($key, $index, $lang = null)
+ {
+ /** @var Language $language */
+ $language = $this->grav['language'];
+
+ return $language->translateArray($key, $index, $lang);
+ }
+
+ /**
+ * Repeat given string x times.
+ *
+ * @param string $input
+ * @param int $multiplier
+ *
+ * @return string
+ */
+ public function repeatFunc($input, $multiplier)
+ {
+ return str_repeat($input, $multiplier);
+ }
+
+ /**
+ * Return URL to the resource.
+ *
+ * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
+ *
+ * @param string $input Resource to be located.
+ * @param bool $domain True to include domain name.
+ * @param bool $failGracefully If true, return URL even if the file does not exist.
+ * @return string|false Returns url to the resource or null if resource was not found.
+ */
+ public function urlFunc($input, $domain = false, $failGracefully = false)
+ {
+ return Utils::url($input, $domain, $failGracefully);
+ }
+
+ /**
+ * This function will evaluate Twig $twig through the $environment, and return its results.
+ *
+ * @param array $context
+ * @param string $twig
+ * @return mixed
+ */
+ public function evaluateTwigFunc($context, $twig)
+ {
+
+ $loader = new FilesystemLoader('.');
+ $env = new Environment($loader);
+ $env->addExtension($this);
+
+ $template = $env->createTemplate($twig);
+
+ return $template->render($context);
+ }
+
+ /**
+ * This function will evaluate a $string through the $environment, and return its results.
+ *
+ * @param array $context
+ * @param string $string
+ * @return mixed
+ */
+ public function evaluateStringFunc($context, $string)
+ {
+ return $this->evaluateTwigFunc($context, "{{ $string }}");
+ }
+
+ /**
+ * Based on Twig\Extension\Debug / twig_var_dump
+ * (c) 2011 Fabien Potencier
+ *
+ * @param Environment $env
+ * @param array $context
+ */
+ public function dump(Environment $env, $context)
+ {
+ if (!$env->isDebug() || !$this->debugger) {
+ return;
+ }
+
+ $count = func_num_args();
+ if (2 === $count) {
+ $data = [];
+ foreach ($context as $key => $value) {
+ if (is_object($value)) {
+ if (method_exists($value, 'toArray')) {
+ $data[$key] = $value->toArray();
+ } else {
+ $data[$key] = 'Object (' . get_class($value) . ')';
+ }
+ } else {
+ $data[$key] = $value;
+ }
+ }
+ $this->debugger->addMessage($data, 'debug');
+ } else {
+ for ($i = 2; $i < $count; $i++) {
+ $var = func_get_arg($i);
+ $this->debugger->addMessage($var, 'debug');
+ }
+ }
+ }
+
+ /**
+ * Output a Gist
+ *
+ * @param string $id
+ * @param string|false $file
+ * @return string
+ */
+ public function gistFunc($id, $file = false)
+ {
+ $url = 'https://gist.github.com/' . $id . '.js';
+ if ($file) {
+ $url .= '?file=' . $file;
+ }
+ return '';
+ }
+
+ /**
+ * Generate a random string
+ *
+ * @param int $count
+ * @return string
+ */
+ public function randomStringFunc($count = 5)
+ {
+ return Utils::generateRandomString($count);
+ }
+
+ /**
+ * Pad a string to a certain length with another string
+ *
+ * @param string $input
+ * @param int $pad_length
+ * @param string $pad_string
+ * @param int $pad_type
+ * @return string
+ */
+ public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT)
+ {
+ return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
+ }
+
+ /**
+ * Workaround for twig associative array initialization
+ * Returns a key => val array
+ *
+ * @param string $key key of item
+ * @param string $val value of item
+ * @param array|null $current_array optional array to add to
+ * @return array
+ */
+ public function arrayKeyValueFunc($key, $val, $current_array = null)
+ {
+ if (empty($current_array)) {
+ return array($key => $val);
+ }
+
+ $current_array[$key] = $val;
+
+ return $current_array;
+ }
+
+ /**
+ * Wrapper for array_intersect() method
+ *
+ * @param array|Collection $array1
+ * @param array|Collection $array2
+ * @return array|Collection
+ */
+ public function arrayIntersectFunc($array1, $array2)
+ {
+ if ($array1 instanceof Collection && $array2 instanceof Collection) {
+ return $array1->intersect($array2)->toArray();
+ }
+
+ return array_intersect($array1, $array2);
+ }
+
+ /**
+ * Translate a string
+ *
+ * @return string
+ */
+ public function translateFunc()
+ {
+ return $this->grav['language']->translate(func_get_args());
+ }
+
+ /**
+ * Authorize an action. Returns true if the user is logged in and
+ * has the right to execute $action.
+ *
+ * @param string|array $action An action or a list of actions. Each
+ * entry can be a string like 'group.action'
+ * or without dot notation an associative
+ * array.
+ * @return bool Returns TRUE if the user is authorized to
+ * perform the action, FALSE otherwise.
+ */
+ public function authorize($action)
+ {
+ // Admin can use Flex users even if the site does not; make sure we use the right version of the user.
+ $admin = $this->grav['admin'] ?? null;
+ if ($admin) {
+ $user = $admin->user;
+ } else {
+ /** @var UserInterface|null $user */
+ $user = $this->grav['user'] ?? null;
+ }
+
+ if (!$user) {
+ return false;
+ }
+
+ if (is_array($action)) {
+ if (Utils::isAssoc($action)) {
+ // Handle nested access structure.
+ $actions = Utils::arrayFlattenDotNotation($action);
+ } else {
+ // Handle simple access list.
+ $actions = array_combine($action, array_fill(0, count($action), true));
+ }
+ } else {
+ // Handle single action.
+ $actions = [(string)$action => true];
+ }
+
+ $count = count($actions);
+ foreach ($actions as $act => $authenticated) {
+ // Ignore 'admin.super' if it's not the only value to be checked.
+ if ($act === 'admin.super' && $count > 1 && $user instanceof FlexObjectInterface) {
+ continue;
+ }
+
+ $auth = $user->authorize($act) ?? false;
+ if (is_bool($auth) && $auth === Utils::isPositive($authenticated)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action.
+ *
+ * For maximum protection, ensure that the string representing the action is as specific as possible
+ *
+ * @param string $action the action
+ * @param string $nonceParamName a custom nonce param name
+ * @return string the nonce input field
+ */
+ public function nonceFieldFunc($action, $nonceParamName = 'nonce')
+ {
+ $string = '';
+
+ return $string;
+ }
+
+ /**
+ * Decodes string from JSON.
+ *
+ * @param string $str
+ * @param bool $assoc
+ * @param int $depth
+ * @param int $options
+ * @return array
+ */
+ public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0)
+ {
+ return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options);
+ }
+
+ /**
+ * Used to retrieve a cookie value
+ *
+ * @param string $key The cookie name to retrieve
+ * @return string
+ */
+ public function getCookie($key)
+ {
+ return filter_input(INPUT_COOKIE, $key, FILTER_SANITIZE_STRING);
+ }
+
+ /**
+ * Twig wrapper for PHP's preg_replace method
+ *
+ * @param string|string[] $subject the content to perform the replacement on
+ * @param string|string[] $pattern the regex pattern to use for matches
+ * @param string|string[] $replace the replacement value either as a string or an array of replacements
+ * @param int $limit the maximum possible replacements for each pattern in each subject
+ * @return string|string[]|null the resulting content
+ */
+ public function regexReplace($subject, $pattern, $replace, $limit = -1)
+ {
+ return preg_replace($pattern, $replace, $subject, $limit);
+ }
+
+ /**
+ * Twig wrapper for PHP's preg_grep method
+ *
+ * @param array $array
+ * @param string $regex
+ * @param int $flags
+ * @return array
+ */
+ public function regexFilter($array, $regex, $flags = 0)
+ {
+ return preg_grep($regex, $array, $flags);
+ }
+
+ /**
+ * Twig wrapper for PHP's preg_match method
+ *
+ * @param string $subject the content to perform the match on
+ * @param string $pattern the regex pattern to use for match
+ * @param int $flags
+ * @param int $offset
+ * @return array|false returns the matches if there is at least one match in the subject for a given pattern or null if not.
+ */
+ public function regexMatch($subject, $pattern, $flags = 0, $offset = 0)
+ {
+ if (preg_match($pattern, $subject, $matches, $flags, $offset) === false) {
+ return false;
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Twig wrapper for PHP's preg_split method
+ *
+ * @param string $subject the content to perform the split on
+ * @param string $pattern the regex pattern to use for split
+ * @param int $limit the maximum possible splits for the given pattern
+ * @param int $flags
+ * @return array|false the resulting array after performing the split operation
+ */
+ public function regexSplit($subject, $pattern, $limit = -1, $flags = 0)
+ {
+ return preg_split($pattern, $subject, $limit, $flags);
+ }
+
+ /**
+ * redirect browser from twig
+ *
+ * @param string $url the url to redirect to
+ * @param int $statusCode statusCode, default 303
+ * @return void
+ */
+ public function redirectFunc($url, $statusCode = 303)
+ {
+ $response = new Response($statusCode, ['location' => $url]);
+
+ $this->grav->close($response);
+ }
+
+ /**
+ * Generates an array containing a range of elements, optionally stepped
+ *
+ * @param int $start Minimum number, default 0
+ * @param int $end Maximum number, default `getrandmax()`
+ * @param int $step Increment between elements in the sequence, default 1
+ * @return array
+ */
+ public function rangeFunc($start = 0, $end = 100, $step = 1)
+ {
+ return range($start, $end, $step);
+ }
+
+ /**
+ * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest,
+ * in which case we may unsafely assume ajax. Non critical use only.
+ *
+ * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest
+ */
+ public function isAjaxFunc()
+ {
+ return (
+ !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
+ && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
+ }
+
+ /**
+ * Get the Exif data for a file
+ *
+ * @param string $image
+ * @param bool $raw
+ * @return mixed
+ */
+ public function exifFunc($image, $raw = false)
+ {
+ if (isset($this->grav['exif'])) {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->grav['locator'];
+
+ if ($locator->isStream($image)) {
+ $image = $locator->findResource($image);
+ }
+
+ $exif_reader = $this->grav['exif']->getReader();
+
+ if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
+ $exif_data = $exif_reader->read($image);
+
+ if ($exif_data) {
+ if ($raw) {
+ return $exif_data->getRawData();
+ }
+
+ return $exif_data->getData();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Simple function to read a file based on a filepath and output it
+ *
+ * @param string $filepath
+ * @return bool|string
+ */
+ public function readFileFunc($filepath)
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->grav['locator'];
+
+ if ($locator->isStream($filepath)) {
+ $filepath = $locator->findResource($filepath);
+ }
+
+ if ($filepath && file_exists($filepath)) {
+ return file_get_contents($filepath);
+ }
+
+ return false;
+ }
+
+ /**
+ * Process a folder as Media and return a media object
+ *
+ * @param string $media_dir
+ * @return Media|null
+ */
+ public function mediaDirFunc($media_dir)
+ {
+ /** @var UniformResourceLocator $locator */
+ $locator = $this->grav['locator'];
+
+ if ($locator->isStream($media_dir)) {
+ $media_dir = $locator->findResource($media_dir);
+ }
+
+ if ($media_dir && file_exists($media_dir)) {
+ return new Media($media_dir);
+ }
+
+ return null;
+ }
+
+ /**
+ * Dump a variable to the browser
+ *
+ * @param mixed $var
+ * @return void
+ */
+ public function vardumpFunc($var)
+ {
+ var_dump($var);
+ }
+
+ /**
+ * Returns a nicer more readable filesize based on bytes
+ *
+ * @param int $bytes
+ * @return string
+ */
+ public function niceFilesizeFunc($bytes)
+ {
+ return Utils::prettySize($bytes);
+ }
+
+ /**
+ * Returns a nicer more readable number
+ *
+ * @param int|float|string $n
+ * @return string|bool
+ */
+ public function niceNumberFunc($n)
+ {
+ if (!is_float($n) && !is_int($n)) {
+ if (!is_string($n) || $n === '') {
+ return false;
+ }
+
+ // Strip any thousand formatting and find the first number.
+ $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n)));
+ $n = reset($list);
+
+ if (!is_numeric($n)) {
+ return false;
+ }
+
+ $n = (float)$n;
+ }
+
+ // now filter it;
+ if ($n > 1000000000000) {
+ return round($n/1000000000000, 2).' t';
+ }
+ if ($n > 1000000000) {
+ return round($n/1000000000, 2).' b';
+ }
+ if ($n > 1000000) {
+ return round($n/1000000, 2).' m';
+ }
+ if ($n > 1000) {
+ return round($n/1000, 2).' k';
+ }
+
+ return number_format($n);
+ }
+
+ /**
+ * 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 array $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 mixed
+ */
+ public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false)
+ {
+ $page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null;
+
+ // 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;
+ }
+
+ return $value;
+ }
+ $page = $page->parent();
+ }
+ }
+
+ if ($exists) {
+ return false;
+ }
+
+ 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 array $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)
+ * @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);
+ }
+
+ /**
+ * takes an array of classes, and if they are not set on body_classes
+ * look to see if they are set in theme config
+ *
+ * @param array $context
+ * @param string|string[] $classes
+ * @return string
+ */
+ public function bodyClassFunc($context, $classes)
+ {
+
+ $header = $context['page']->header();
+ $body_classes = $header->body_classes ?? '';
+
+ foreach ((array)$classes as $class) {
+ if (!empty($body_classes) && Utils::contains($body_classes, $class)) {
+ continue;
+ }
+
+ $val = $this->config->get('theme.' . $class, false) ? $class : false;
+ $body_classes .= $val ? ' ' . $val : '';
+ }
+
+ return $body_classes;
+ }
+
+ /**
+ * Returns the content of an SVG image and adds extra classes as needed
+ *
+ * @param string $path
+ * @param string|null $classes
+ * @return string|string[]|null
+ */
+ public static function svgImageFunction($path, $classes = null, $strip_style = false)
+ {
+ $path = Utils::fullPath($path);
+
+ $classes = $classes ?: '';
+
+ if (file_exists($path) && !is_dir($path)) {
+ $svg = file_get_contents($path);
+ $classes = " inline-block $classes";
+ $matched = false;
+
+ //Remove xml tag if it exists
+ $svg = preg_replace('/^<\?xml.*\?>/','', $svg);
+
+ //Strip style if needed
+ if ($strip_style) {
+ $svg = preg_replace('//s', '', $svg);
+ }
+
+ //Look for existing class
+ $svg = preg_replace_callback('/^