Merge branch 'release/0.9.27'

This commit is contained in:
Andy Miller
2015-05-09 13:14:33 -06:00
43 changed files with 745 additions and 360 deletions

2
.gitignore vendored
View File

@@ -7,6 +7,8 @@ vendor/
.sass-cache
# Grav Specific
backup/*
!backup/.*
cache/*
!cache/.*
assets/*

View File

@@ -44,7 +44,7 @@ RewriteRule .* index.php [L]
## Begin - Security
# Block all direct access for these folders
RewriteRule ^(cache|bin|logs)/(.*) error [L]
RewriteRule ^(cache|bin|logs|backup)/(.*) error [L]
# Block access to specific file types for these folders
RewriteRule ^(system|user|vendor)/(.*)\.(txt|md|html|yaml|php|twig|sh|bat)$ error [L]
## End - Security

View File

@@ -1,3 +1,32 @@
# v0.9.27
## 05/09/2015
1. [](#new)
* Added new composer CLI command
* Added page-level summary header overrides
* Added `size` back for Media objects
* Refactored Backup command in preparation for admin plugin
* Added a new `parseLinks` method to Plugins class
* Added `starts_with` and `ends_with` Twig filters
2. [](#improved)
* Opitmized install of vendor libraries for speed improvement
* Improved configuration handling in preparation for admin plugin
* Cache optimization: Don't cache Twig templates when you pass dynamic params
* Moved `Utils::rcopy` to `Folder::rcopy`
* Improved `Folder::doDelete`
* Added check for required Curl in GPM
* Updated included composer.phar to latest version
* Various blueprint fixes for admin plugin
* Various PSR and code cleanup tasks
3. [](#bugfix)
* Fix issue with Gzip not working with `onShutDown()` event
* Fix for URLs with trailing slashes
* Handle condition where certain errors resulted in blank page
* Fix for issue with theme name equal to base_url and asset pipeline
* Fix to properly nomralize font rewrite path
* Fix for absolute URLs below the current page
* Fix for `..` page references
# v0.9.26
## 04/24/2015

0
backup/.gitkeep Normal file
View File

Binary file not shown.

View File

@@ -9,7 +9,7 @@ if (version_compare($ver = PHP_VERSION, $req = '5.4.0', '<')) {
if (!file_exists(__DIR__ . '/../vendor')){
// Before we can even start, we need to run composer first
echo "Preparing to install vendor dependencies...\n\n";
echo system('php bin/composer.phar --working-dir="'.__DIR__.'/../" --no-interaction install');
echo system('php bin/composer.phar --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
echo "\n\n";
}
@@ -26,6 +26,10 @@ if (!file_exists(ROOT_DIR . 'index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
if (!function_exists('curl_version')) {
exit('FATAL: GPM requires PHP Curl module to be installed');
}
$grav = Grav::instance(array('loader' => $autoload));
$grav['config']->init();
$grav['streams'];

View File

@@ -9,7 +9,7 @@ if (version_compare($ver = PHP_VERSION, $req = '5.4.0', '<')) {
if (!file_exists(__DIR__ . '/../vendor')){
// Before we can even start, we need to run composer first
echo "Preparing to install vendor dependencies...\n\n";
echo system('php bin/composer.phar --working-dir="'.__DIR__.'/../" --no-interaction install');
echo system('php bin/composer.phar --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
echo "\n\n";
}
@@ -28,6 +28,7 @@ if (!file_exists(ROOT_DIR . 'index.php')) {
$app = new Application('Grav CLI Application', '0.1.0');
$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(),

View File

@@ -25,7 +25,7 @@ http {
index index.php;
if (!-e $request_filename){ rewrite ^(.*)$ /index.php last; }
}
location /images/ {
# Serve images as static
}
@@ -44,6 +44,10 @@ http {
rewrite ^/bin/(.*)$ /error redirect;
}
location /backup {
rewrite ^/backup/(.*) /error redirect;
}
location /system {
rewrite ^/system/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
}

View File

@@ -24,6 +24,8 @@ form:
type: text
size: large
label: Default Email
validate:
type: email
taxonomies:
type: text

View File

@@ -27,7 +27,7 @@ form:
label: Default Theme
help: "Set the theme (defaults to 'default')"
pages.markdown_extra:
pages.markdown.extra:
type: toggle
label: Markdown Extra
highlight: 1
@@ -101,6 +101,16 @@ form:
validate:
type: number
min: 1
pages.publish_dates:
type: toggle
label: Date-based publishing
help: Automatically (un)publish posts based on their date
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
@@ -175,6 +185,36 @@ form:
label: Cache Prefix
placeholder: "Derived from base URL (override by entering random string)"
cache.gzip:
type: toggle
label: GZIP compression
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
assets.enable_asset_timestamp:
type: toggle
label: Enable timestamps on assets
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
media.enable_media_timestamp:
type: toggle
label: Enable timestamps on media
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
twig:
type: section
title: Twig Templating
@@ -287,6 +327,32 @@ form:
validate:
type: bool
errors:
type: section
title: Error handler
underline: true
fields:
errors.display:
type: toggle
label: Display errors
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
errors.log:
type: toggle
label: Log errors
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
debugger:
type: section
title: Debugger
@@ -362,3 +428,59 @@ form:
0: No
validate:
type: bool
media:
type: section
title: Media
underline: true
fields:
images.default_image_quality:
type: text
label: Default image quality
classes: x-small
validate:
type: number
min: 1
max: 100
images.debug:
type: toggle
label: Image debug watermark
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
media.upload_limit:
type: text
label: File upload limit
classes: small
validate:
type: number
system:
type: section
title: System
underline: true
fields:
timezone:
type: select
label: Timezone
classes: fancy
@data-options: '\Grav\Common\Utils::timezones'
default: ''
options:
'': '- None -'
param_sep:
type: select
label: Parameter separator
classes: fancy
default: ''
options:
':': ': (default)'
';': '; (use this for apache on Windows)'

View File

@@ -61,7 +61,7 @@ form:
@data-options: '\Grav\Common\Page\Pages::parents'
@data-default: '\Grav\Plugin\admin::route'
options:
'/': '- Root -'
'': '- Select -'
validate:
required: true

View File

@@ -28,8 +28,6 @@ form:
validate:
required: true
fullname:
type: text
size: large
@@ -41,46 +39,3 @@ form:
type: text
size: large
label: Title
admin:
type: section
title: Admin Access
fields:
access.admin.super:
type: toggle
label: Super user
default: 0
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
access.admin.login:
type: toggle
label: Admin login
default: 0
highlight: 1
options:
1: Yes
0: No
validate:
type: bool
site:
type: section
title: Site Access
fields:
access.site.login:
type: toggle
label: Site login
default: 1
highlight: 1
options:
1: Yes
0: No
validate:
type: bool

View File

@@ -1,18 +1,31 @@
title: Grav # Name of the site
author:
name: John Appleseed # Default author name
email: 'john@email.com' # Default author email
taxonomies: [category,tag] # Arbitrary list of taxonomy types
blog:
route: '/blog' # Route to blog
route: '/blog' # Route to blog (deprecated)
metadata:
description: 'My Grav Site' # Site description
summary:
enabled: true # enable or disable summary of page
format: short # long = summary delimiter will be ignored; short = use the first occurence of delimter or size
size: 300 # Maximum length of summary (characters)
delimiter: === # The summary delimiter
routes:
/something/else: '/blog/sample-3' # Alias for /blog/sample-3
/another/one/here: '/blog/sample-3' # Another alias for /blog/sample-3
/new/*: '/blog/*' # Wildcard any /new/my-page URL to /blog/my-page Route
#menu: # Sample Menu Example
# - text: Source
# icon: github
# url: https://github.com/getgrav/grav
# - icon: twitter
# url: http://twitter.com/getgrav

View File

@@ -8,7 +8,7 @@ home:
pages:
theme: antimatter # Default theme (defaults to "antimatter" theme)
order:
by: defaults # Order pages by "default", "alpha" or "date"
by: default # Order pages by "default", "alpha" or "date"
dir: asc # Default ordering direction, "asc" or "desc"
list:
count: 20 # Default item count per page
@@ -78,6 +78,7 @@ images:
media:
enable_media_timestamp: false # Enable media timetsamps
upload_limit: 0 # Set maximum upload size in bytes (0 is unlimited)
security:
default_hash: $2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK

View File

@@ -2,7 +2,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '0.9.26');
define('GRAV_VERSION', '0.9.27');
define('DS', '/');
// Directories and Paths

View File

@@ -825,7 +825,7 @@ class Assets
} else {
// Fix to remove relative dir if grav is in one
if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
$relative_path = str_replace($this->base_url, '/', $link);
$relative_path = ltrim(preg_replace($this->base_url, '/', $link, 1), '/');
}
$relative_dir = dirname($relative_path);
@@ -881,18 +881,7 @@ class Assets
return $matches[0];
}
$newpath = array();
$paths = explode('/', $old_url);
foreach ($paths as $path) {
if ($path == '..') {
$relative_path = dirname($relative_path);
} else {
$newpath[] = $path;
}
}
$new_url = rtrim($this->base_url, '/') . $relative_path . '/' . implode('/', $newpath);
$new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
return str_replace($old_url, $new_url, $matches[0]);
},

View File

@@ -0,0 +1,121 @@
<?php
namespace Grav\Common\Backup;
use Grav\Common\GravTrait;
use Grav\Common\Filesystem\Folder;
/**
* The ZipBackup class lets you create simple zip-backups of a grav site
*
* @author RocketTheme
* @license MIT
*/
class ZipBackup
{
use GravTrait;
protected static $ignorePaths = [
'backup',
'cache',
'images',
'logs'
];
protected static $ignoreFolders = [
'.git'
];
public static function backup($destination = null, callable $messager = null)
{
if (!$destination) {
$destination = self::getGrav()['locator']->findResource('backup://', true);
if (!$destination)
throw new \RuntimeException('The backup folder is missing.');
Folder::mkdir($destination);
}
$name = self::getGrav()['config']->get('site.title', basename(GRAV_ROOT));
if (is_dir($destination)) {
$date = date('YmdHis', time());
$filename = $name . '-' . $date . '.zip';
$destination = rtrim($destination, DS) . DS . $filename;
}
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => 'Creating new Backup "' . $destination . '"'
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => ''
]);
$zip = new \ZipArchive();
$zip->open($destination, \ZipArchive::CREATE);
static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
$messager && $messager([
'type' => 'progress',
'percentage' => false,
'complete' => true
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => ''
]);
$messager && $messager([
'type' => 'message',
'level' => 'info',
'message' => 'Saving and compressing archive...'
]);
$zip->close();
return $destination;
}
/**
* @param $folder
* @param $zipFile
* @param $exclusiveLength
* @param $messager
*/
private static function folderToZip($folder, \ZipArchive &$zipFile, $exclusiveLength, callable $messager = null)
{
$handle = opendir($folder);
while (false !== $f = readdir($handle)) {
if ($f != '.' && $f != '..') {
$filePath = "$folder/$f";
// Remove prefix from file path before add to zip.
$localPath = substr($filePath, $exclusiveLength);
if (in_array($f, static::$ignoreFolders) || in_array($localPath, static::$ignorePaths)) {
continue;
}
if (is_file($filePath)) {
$zipFile->addFile($filePath, $localPath);
$messager && $messager([
'type' => 'progress',
'percentage' => false,
'complete' => false
]);
} elseif (is_dir($filePath)) {
// Add sub-directory.
$zipFile->addEmptyDir($localPath);
static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
}
}
}
closedir($handle);
}
}

View File

@@ -72,9 +72,17 @@ class Config extends Data
'prefixes' => [
'' => ['logs']
]
],
'backup' => [
'type' => 'Stream',
'prefixes' => [
'' => ['backup']
]
]
];
protected $setup = [];
protected $blueprintFiles = [];
protected $configFiles = [];
protected $checksum;
@@ -88,33 +96,25 @@ class Config extends Data
protected $environment;
protected $messages = [];
public function __construct(array $items = array(), Grav $grav = null, $environment = null)
public function __construct(array $setup = array(), Grav $grav = null, $environment = null)
{
$this->grav = $grav ?: Grav::instance();
$this->finder = new ConfigFinder;
$this->environment = $environment ?: 'localhost';
$this->messages[] = 'Environment Name: ' . $this->environment;
if (isset($items['@class'])) {
if ($items['@class'] != get_class($this)) {
throw new \InvalidArgumentException('Unrecognized config cache file!');
}
// Loading pre-compiled configuration.
$this->timestamp = (int) $items['timestamp'];
$this->checksum = $items['checksum'];
$this->items = (array) $items['data'];
} else {
// Make sure that
if (!isset($items['streams']['schemes'])) {
$items['streams']['schemes'] = [];
}
$items['streams']['schemes'] += $this->streams;
$items = $this->autoDetectEnvironmentConfig($items);
$this->messages[] = $items['streams']['schemes']['config']['prefixes'][''];
parent::__construct($items);
// Make sure that
if (!isset($setup['streams']['schemes'])) {
$setup['streams']['schemes'] = [];
}
$setup['streams']['schemes'] += $this->streams;
$setup = $this->autoDetectEnvironmentConfig($setup);
$this->messages[] = $setup['streams']['schemes']['config']['prefixes'][''];
$this->setup = $setup;
parent::__construct($setup);
$this->check();
}
@@ -125,8 +125,10 @@ class Config extends Data
public function reload()
{
$this->items = $this->setup;
$this->check();
$this->init();
$this->debug();
return $this;
}
@@ -150,6 +152,7 @@ class Config extends Data
foreach ($this->messages as $message) {
$this->grav['debugger']->addMessage($message);
}
$this->messages = [];
}
public function init()
@@ -161,15 +164,6 @@ class Config extends Data
$this->blueprintLookup = $locator->findResources('blueprints://config');
$this->pluginLookup = $locator->findResources('plugins://');
if (!isset($this->checksum)) {
$this->messages[] = 'No cached configuration, compiling new configuration..';
} elseif ($this->checksum() != $this->checksum) {
$this->messages[] = 'Configuration checksum mismatch, reloading configuration..';
} else {
$this->messages[] = 'Configuration checksum matches, using cached version.';
return;
}
$this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master');
$this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master');
@@ -255,7 +249,6 @@ class Config extends Data
'files' => $blueprintFiles,
'data' => $this->blueprints->toArray()
];
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$this->messages[] = 'Saving compiled blueprints.';
@@ -269,44 +262,51 @@ class Config extends Data
protected function loadCompiledConfig($configs, $plugins, $filename = null)
{
$checksum = md5(json_encode($configs));
$filename = $filename
? CACHE_DIR . 'compiled/config/' . $filename . '-' . $this->environment . '.php'
: CACHE_DIR . 'compiled/config/' . $checksum . '-' . $this->environment . '.php';
$file = PhpFile::instance($filename);
$cache = $file->exists() ? $file->content() : null;
$configFiles = $this->finder->locateConfigFiles($configs, $plugins);
$checksum .= ':'.md5(json_encode($configFiles));
$class = get_class($this);
$checksum = $this->checksum();
// Load real file if cache isn't up to date (or is invalid).
if (
!is_array($cache)
|| !isset($cache['checksum'])
|| !isset($cache['@class'])
|| $cache['checksum'] != $checksum
|| $cache['@class'] != $class
) {
// Attempt to lock the file for writing.
$file->lock(false);
$this->messages[] = 'No cached configuration, compiling new configuration..';
} else if ($cache['checksum'] !== $checksum) {
$this->messages[] = 'Configuration checksum mismatch, reloading configuration..';
} else {
$this->messages[] = 'Configuration checksum matches, using cached version.';
// Load configuration.
foreach ($configFiles as $files) {
$this->loadConfigFiles($files);
}
$cache = [
'@class' => $class,
'timestamp' => time(),
'checksum' => $this->checksum(),
'data' => $this->toArray()
];
$this->items = $cache['data'];
return;
}
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$this->messages[] = 'Saving compiled configuration.';
$file->save($cache);
$file->unlock();
}
$configFiles = $this->finder->locateConfigFiles($configs, $plugins);
// Attempt to lock the file for writing.
$file->lock(false);
// Load configuration.
foreach ($configFiles as $files) {
$this->loadConfigFiles($files);
}
$cache = [
'@class' => $class,
'timestamp' => time(),
'checksum' => $checksum,
'data' => $this->toArray()
];
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$this->messages[] = 'Saving compiled configuration.';
$file->save($cache);
$file->unlock();
}
$this->items = $cache['data'];

View File

@@ -278,6 +278,46 @@ abstract class Folder
}
}
/**
* Recursive copy of one directory to another
*
* @param $src
* @param $dest
*
* @return bool
*/
public static function rcopy($src, $dest)
{
// If the src is not a directory do a simple file copy
if (!is_dir($src)) {
copy($src, $dest);
return true;
}
// If the destination directory does not exist create it
if (!is_dir($dest)) {
if (!mkdir($dest)) {
// If the destination directory could not be created stop processing
return false;
}
}
// Open the source directory to read in files
$i = new \DirectoryIterator($src);
/** @var \DirectoryIterator $f */
foreach ($i as $f) {
if ($f->isFile()) {
copy($f->getRealPath(), "$dest/" . $f->getFilename());
} else {
if (!$f->isDot() && $f->isDir()) {
static::rcopy($f->getRealPath(), "$dest/$f");
}
}
}
return true;
}
/**
* @param string $folder
* @return bool
@@ -290,13 +330,24 @@ abstract class Folder
return @unlink($folder);
}
// Go through all items in filesystem and recursively remove everything.
$files = array_diff(scandir($folder), array('.', '..'));
foreach ($files as $file) {
$path = "{$folder}/{$file}";
(is_dir($path)) ? self::doDelete($path) : @unlink($path);
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
/** @var \DirectoryIterator $fileinfo */
foreach ($files as $fileinfo) {
if ($fileinfo->isDir()) {
if (false === rmdir($fileinfo->getRealPath())) {
return false;
}
} else {
if (false === unlink($fileinfo->getRealPath())) {
return false;
}
}
}
return @rmdir($folder);
return rmdir($folder);
}
}

View File

@@ -1,7 +1,6 @@
<?php
namespace Grav\Common;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Pages;
use Grav\Common\Service\ConfigServiceProvider;
@@ -78,7 +77,7 @@ class Grav extends Container
return new Cache($c);
};
$container['plugins'] = function ($c) {
return new Plugins($c);
return new Plugins();
};
$container['themes'] = function ($c) {
return new Themes($c);
@@ -102,7 +101,8 @@ class Grav extends Container
/** @var Uri $uri */
$uri = $c['uri'];
$path = $uri->path();
$path = rtrim($uri->path(), '/');
$path = $path ?: '/';
$page = $pages->dispatch($path);
@@ -168,12 +168,6 @@ class Grav extends Container
public function process()
{
// Use output buffering to prevent headers from being sent too early.
ob_start();
if ($this['config']->get('system.cache.gzip')) {
ob_start('ob_gzhandler');
}
/** @var Debugger $debugger */
$debugger = $this['debugger'];
@@ -186,6 +180,12 @@ class Grav extends Container
$this['config']->debug();
$debugger->stopTimer('_config');
// Use output buffering to prevent headers from being sent too early.
ob_start();
if ($this['config']->get('system.cache.gzip')) {
ob_start('ob_gzhandler');
}
// Initialize the timezone
if ($this['config']->get('system.timezone')) {
date_default_timezone_set($this['config']->get('system.timezone'));
@@ -350,26 +350,31 @@ class Grav extends Container
public function shutdown()
{
if ($this['config']->get('system.debugger.shutdown.close_connection')) {
//stop user abort
if (function_exists('ignore_user_abort')) {
@ignore_user_abort(true);
}
// close the session
if (isset($this['session'])) {
$this['session']->close();
}
// flush buffer if gzip buffer was started
if ($this['config']->get('system.cache.gzip')) {
ob_end_flush(); // gzhandler buffer
}
// get lengh and close the connection
header('Content-Length: ' . ob_get_length());
header("Connection: close\r\n");
header("Connection: close");
ob_end_flush(); // regular buffer
// flush the regular buffer
ob_end_flush();
@ob_flush();
flush();
// fix for fastcgi close connection issue
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
}

View File

@@ -37,7 +37,7 @@ trait ParsedownGravTrait
$this->pages_dir = self::getGrav()['locator']->findResource('page://');
$this->special_chars = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
if ($defaults == null) {
if ($defaults === null) {
$defaults = self::getGrav()['config']->get('system.pages.markdown');
}
@@ -232,13 +232,14 @@ trait ParsedownGravTrait
$normalized_path = Utils::normalizePath($this->pages_dir . $markdown_url);
$normalized_url = Utils::normalizePath($this->base_url . $markdown_url);
} else {
// contains path, so need to normalize it
if (Utils::contains($markdown_url, '/')) {
$normalized_path = Utils::normalizePath($this->page->path() . '/' . $markdown_url);
} else {
$normalized_path = false;
}
$normalized_url = $this->base_url . Utils::normalizePath($this->page->route() . '/' . $markdown_url);
$normalized_path = Utils::normalizePath($this->page->path() . '/' . $markdown_url);
}
// special check to see if path checking is required.
$just_path = str_replace($normalized_url, '', $normalized_path);
if ($just_path == $this->page->path()) {
return $normalized_url;
}
// if this file exits, get the page and work with that
@@ -251,16 +252,23 @@ trait ParsedownGravTrait
$page_path = $path_info['dirname'];
$filename = '';
// save the filename if a file is part of the path
$filename_regex = "/([\w\d-_]+\.([a-zA-Z]{2,4}))$/";
if (preg_match($filename_regex, $full_path, $matches)) {
if ($matches[2] != 'md') {
$filename = '/' . $matches[1];
}
} else {
if ($markdown_url == '..') {
$page_path = $full_path;
} else {
// save the filename if a file is part of the path
$filename_regex = "/([\w\d-_]+\.([a-zA-Z]{2,4}))$/";
if (preg_match($filename_regex, $full_path, $matches)) {
if ($matches[2] != 'md') {
$filename = '/' . $matches[1];
}
} else {
$page_path = $full_path;
}
}
// get page instances and try to find one that fits
$instances = $this->pages->instances();
if (isset($instances[$page_path])) {

View File

@@ -47,7 +47,7 @@ class Media extends Getters
/** @var \DirectoryIterator $info */
foreach ($iterator as $path => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() == 'md') {
if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename() === '.DS_Store') {
continue;
}
@@ -58,27 +58,36 @@ class Media extends Getters
if ($type === 'alternative') {
$media["{$basename}.{$ext}"][$type] = isset($media["{$basename}.{$ext}"][$type]) ? $media["{$basename}.{$ext}"][$type] : [];
$media["{$basename}.{$ext}"][$type][$extra] = $path;
$media["{$basename}.{$ext}"][$type][$extra] = [ 'file' => $path, 'size' => $info->getSize() ];
} else {
$media["{$basename}.{$ext}"][$type] = $path;
$media["{$basename}.{$ext}"][$type] = [ 'file' => $path, 'size' => $info->getSize() ];
}
}
foreach ($media as $name => $types) {
// First prepare the alternatives in case there is no base medium
if (!empty($types['alternative'])) {
foreach ($types['alternative'] as $ratio => &$file) {
$file = MediumFactory::fromFile($file);
foreach ($types['alternative'] as $ratio => &$alt) {
$alt['file'] = MediumFactory::fromFile($alt['file']);
if (!$alt['file']) {
unset($types['alternative'][$ratio]);
} else {
$alt['file']->set('size', $alt['size']);
}
}
}
// Create the base medium
if (!empty($types['base'])) {
$medium = MediumFactory::fromFile($types['base']);
$medium = MediumFactory::fromFile($types['base']['file']);
$medium && $medium->set('size', $types['base']['size']);
} else if (!empty($types['alternative'])) {
$altMedium = reset($types['alternative']);
$ratio = key($types['alternative']);
$altMedium = $altMedium['file'];
$medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1);
}
@@ -87,13 +96,13 @@ class Media extends Getters
}
if (!empty($types['meta'])) {
$medium->addMetaFile($types['meta']);
$medium->addMetaFile($types['meta']['file']);
}
if (!empty($types['thumb'])) {
// We will not turn it into medium yet because user might never request the thumbnail
// not wasting any resources on that, maybe we should do this for medium in general?
$medium->set('thumbnails.page', $types['thumb']);
$medium->set('thumbnails.page', $types['thumb']['file']);
}
// Build missing alternatives

View File

@@ -308,7 +308,7 @@ class ImageMedium extends Medium
}
try {
$result = call_user_func_array([$this->image, $method], $args);
call_user_func_array([$this->image, $method], $args);
foreach ($this->alternatives as $ratio => $medium) {
$args_copy = $args;

View File

@@ -60,6 +60,6 @@ class Link implements RenderableInterface
// Don't start nesting links, if user has multiple link calls in his
// actions, we will drop the previous links.
return $this->source instanceof LinkMedium ? $this->source : $this;
return $this->source instanceof Link ? $this->source : $this;
}
}

View File

@@ -399,7 +399,7 @@ class Medium extends Data implements RenderableInterface
$thumb = $this->get('thumbnails.' . $type, false);
if ($thumb) {
$thumb = $thumb instanceof ThumbnailMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
$thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
$thumb->parent = $this;
}

View File

@@ -303,21 +303,25 @@ class Page
public function summary($size = null)
{
/** @var Config $config */
$config = self::getGrav()['config'];
$config = self::getGrav()['config']->get('site.summary');
$content = $this->content();
if (isset($this->header->summary)) {
$config = array_merge($config, $this->header->summary);
}
// Return summary based on settings in site config file
if (!$config->get('site.summary.enabled', true)) {
if (!$config['enabled']) {
return $content;
}
// Get summary size from site config's file
if (is_null($size)) {
$size = $config->get('site.summary.size', null);
$size = $config['size'];
}
// Return calculated summary based on summary divider's position
$format = $config->get('site.summary.format', 'short');
$format = $config['format'];
// Return entire page content on wrong/ unknown format
if (!in_array($format, array('short', 'long'))) {
return $content;

View File

@@ -114,6 +114,25 @@ class Plugin implements EventSubscriberInterface
}
}
/**
* This function will search a string for markdown links in a specific format. The link value can be
* optionally compared against via the $internal_regex and operated on by the callback $function
* provided.
*
* format: [plugin:myplugin_name](function_data)
*
* @param $content The string to perform operations upon
* @param $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';
return preg_replace_callback($regex, $function, $content);
}
/**
* Merge global and page configurations.
*

View File

@@ -4,6 +4,7 @@ namespace Grav\Common;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprints;
use Grav\Common\Data\Data;
use Grav\Common\GravTrait;
use Grav\Common\File\CompiledYamlFile;
use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\Event\EventSubscriberInterface;
@@ -17,12 +18,7 @@ use RocketTheme\Toolbox\Event\EventSubscriberInterface;
*/
class Plugins extends Iterator
{
protected $grav;
public function __construct(Grav $grav)
{
$this->grav = $grav;
}
use GravTrait;
/**
* Recurses through the plugins directory creating Plugin objects for each plugin it finds.
@@ -33,11 +29,11 @@ class Plugins extends Iterator
public function init()
{
/** @var Config $config */
$config = $this->grav['config'];
$config = self::getGrav()['config'];
$plugins = (array) $config->get('plugins');
/** @var EventDispatcher $events */
$events = $this->grav['events'];
$events = self::getGrav()['events'];
foreach ($plugins as $plugin => $data) {
if (empty($data['enabled'])) {
@@ -45,9 +41,10 @@ class Plugins extends Iterator
continue;
}
$filePath = $this->grav['locator']('plugins://' . $plugin . DS . $plugin . PLUGIN_EXT);
$locator = self::getGrav()['locator'];
$filePath = $locator->findResource('plugins://' . $plugin . DS . $plugin . PLUGIN_EXT);
if (!is_file($filePath)) {
$this->grav['log']->addWarning(sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $plugin));
self::getGrav()['log']->addWarning(sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $plugin));
continue;
}
@@ -70,7 +67,7 @@ class Plugins extends Iterator
throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $plugin));
}
$instance = new $pluginClassName($plugin, $this->grav, $config);
$instance = new $pluginClassName($plugin, self::getGrav(), $config);
if ($instance instanceof EventSubscriberInterface) {
$events->addSubscriber($instance);
}
@@ -123,10 +120,10 @@ class Plugins extends Iterator
$obj = new Data($file->content(), $blueprint);
// Override with user configuration.
$file = CompiledYamlFile::instance("user://config/plugins/{$name}.yaml");
$obj->merge($file->content());
$obj->merge(self::getGrav()['config']->get('plugins.' . $name) ?: []);
// Save configuration always to user/config.
$file = CompiledYamlFile::instance("config://plugins/{$name}.yaml");
$obj->file($file);
return $obj;

View File

@@ -38,18 +38,8 @@ class ConfigServiceProvider implements ServiceProviderInterface
public function loadMasterConfig(Container $container)
{
$environment = $this->getEnvironment($container);
$file = CACHE_DIR . 'compiled/config/master-'.$environment.'.php';
$data = is_file($file) ? (array) include $file : [];
if ($data) {
try {
$config = new Config($data, $container, $environment);
} catch (\Exception $e) {
}
}
if (!isset($config)) {
$config = new Config($this->setup, $container, $environment);
}
$config = new Config($this->setup, $container, $environment);
return $config;
}

View File

@@ -30,7 +30,11 @@ class ErrorServiceProvider implements ServiceProviderInterface
$logger = $container['log'];
$errors->pushHandler(function (\Exception $exception, $inspector, $run) use ($logger) {
$logger->addCritical($exception->getMessage(). ' - Trace: '. $exception->getTraceAsString());
try {
$logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString());
} catch (\Exception $e) {
echo $e;
}
}, 'log');
$errors->register();

View File

@@ -106,10 +106,10 @@ class Themes extends Iterator
$obj = new Data($file->content(), $blueprint);
// Override with user configuration.
$file = CompiledYamlFile::instance("user://config/themes/{$name}" . YAML_EXT);
$obj->merge($file->content());
$obj->merge($this->grav['config']->get('themes.' . $name) ?: []);
// Save configuration always to user/config.
$file = CompiledYamlFile::instance("config://themes/{$name}" . YAML_EXT);
$obj->file($file);
return $obj;

View File

@@ -292,6 +292,12 @@ class Twig
$twig_vars['content'] = $page->content();
$ext = '.' . ($format ? $format : 'html') . TWIG_EXT;
// determine if params are set, if so disable twig cache
$params = $this->grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->template($page->template() . $ext);

View File

@@ -52,7 +52,9 @@ class TwigExtension extends \Twig_Extension
new \Twig_SimpleFilter('contains', [$this, 'containsFilter']),
new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFilter'])
new \Twig_SimpleFilter('markdown', [$this, 'markdownFilter']),
new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter'])
];
}
@@ -347,6 +349,16 @@ class TwigExtension extends \Twig_Extension
return $string;
}
public function startsWithFilter($needle, $haystack)
{
return Utils::startsWith($needle, $haystack);
}
public function endsWithFilter($needle, $haystack)
{
return Utils::endsWith($needle, $haystack);
}
/**
* Repeat given string x times.
*

View File

@@ -193,20 +193,27 @@ class Uri
* Return all or a single query parameter as a URI compatible string.
*
* @param string $id Optional parameter name.
* @param boolean $array return the array format or not
* @return null|string
*/
public function params($id = null)
public function params($id = null, $array = false)
{
$config = Grav::instance()['config'];
$params = null;
if ($id === null) {
if ($array) {
return $this->params;
}
$output = array();
foreach ($this->params as $key => $value) {
$output[] = $key . $config->get('system.param_sep') . $value;
$params = '/'.implode('/', $output);
}
} elseif (isset($this->params[$id])) {
if ($array) {
return $this->params[$id];
}
$params = "/{$id}". $config->get('system.param_sep') . $this->params[$id];
}

View File

@@ -54,75 +54,6 @@ abstract class Utils
return (object) array_merge((array) $obj1, (array) $obj2);
}
/**
* Recursive remove a directory - DANGEROUS! USE WITH CARE!!!!
*
* @param $dir
* @return bool
*/
public static function rrmdir($dir)
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
/** @var \DirectoryIterator $fileinfo */
foreach ($files as $fileinfo) {
if ($fileinfo->isDir()) {
if (false === rmdir($fileinfo->getRealPath())) {
return false;
}
} else {
if (false === unlink($fileinfo->getRealPath())) {
return false;
}
}
}
return rmdir($dir);
}
/**
* Recursive copy of one directory to another
*
* @param $src
* @param $dest
*
* @return bool
*/
public static function rcopy($src, $dest)
{
// If the src is not a directory do a simple file copy
if (!is_dir($src)) {
copy($src, $dest);
return true;
}
// If the destination directory does not exist create it
if (!is_dir($dest)) {
if (!mkdir($dest)) {
// If the destination directory could not be created stop processing
return false;
}
}
// Open the source directory to read in files
$i = new \DirectoryIterator($src);
/** @var \DirectoryIterator $f */
foreach ($i as $f) {
if ($f->isFile()) {
copy($f->getRealPath(), "$dest/" . $f->getFilename());
} else {
if (!$f->isDot() && $f->isDir()) {
static::rcopy($f->getRealPath(), "$dest/$f");
}
}
}
return true;
}
/**
* Truncate HTML by text length.
*
@@ -250,12 +181,9 @@ abstract class Utils
$file_parts = pathinfo($file);
$filesize = filesize($file);
$range = false;
set_time_limit(0);
ignore_user_abort(false);
ini_set('output_buffering', 0);
ini_set('zlib.output_compression', 0);
if ($force_download) {
header('Content-Description: File Transfer');
@@ -420,4 +348,32 @@ abstract class Utils
}
return $root . implode('/', $ret);
}
public static function timezones()
{
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
$offsets = [];
$testDate = new \DateTime;
foreach ($timezones as $zone) {
$tz = new \DateTimeZone($zone);
$offsets[$zone] = $tz->getOffset($testDate);
}
asort($offsets);
$timezone_list = array();
foreach( $offsets as $timezone => $offset )
{
$offset_prefix = $offset < 0 ? '-' : '+';
$offset_formatted = gmdate( 'H:i', abs($offset) );
$pretty_offset = "UTC${offset_prefix}${offset_formatted}";
$timezone_list[$timezone] = "(${pretty_offset}) $timezone";
}
return $timezone_list;
}
}

View File

@@ -7,6 +7,7 @@ use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Grav\Common\Backup\ZipBackup;
/**
* Class BackupCommand
@@ -23,6 +24,10 @@ class BackupCommand extends Command
* @var
*/
protected $progress;
/**
* @var
*/
protected $output;
/**
*
@@ -52,6 +57,8 @@ class BackupCommand extends Command
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
$output->getFormatter()->setStyle('red', new OutputFormatterStyle('red'));
$output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan'));
$output->getFormatter()->setStyle('green', new OutputFormatterStyle('green'));
@@ -60,25 +67,10 @@ class BackupCommand extends Command
$this->progress = new ProgressBar($output);
$this->progress->setFormat('Archiving <cyan>%current%</cyan> files [<green>%bar%</green>] %elapsed:6s% %memory:6s%');
$name = basename($this->source);
$dir = dirname($this->source);
$date = date('YmdHis', time());
$filename = $name . '-' . $date . '.zip';
$destination = ($input->getArgument('destination')) ? $input->getArgument('destination') : ROOT_DIR;
$destination = rtrim($destination, DS) . DS . $filename;
$output->writeln('');
$output->writeln('Creating new Backup "' . $destination . '"');
$this->progress->start();
ZipBackup::backup($destination, [$this, 'output']);
$zip = new \ZipArchive();
$zip->open($destination, \ZipArchive::CREATE);
$zip->addEmptyDir($name);
$this->folderToZip($this->source, $zip, strlen($dir . DS), $this->progress);
$zip->close();
$this->progress->finish();
$output->writeln('');
$output->writeln('');
@@ -90,25 +82,20 @@ class BackupCommand extends Command
* @param $exclusiveLength
* @param $progress
*/
private static function folderToZip($folder, \ZipArchive &$zipFile, $exclusiveLength, ProgressBar $progress)
public function output($args)
{
$handle = opendir($folder);
while (false !== $f = readdir($handle)) {
if ($f != '.' && $f != '..') {
$filePath = "$folder/$f";
// Remove prefix from file path before add to zip.
$localPath = substr($filePath, $exclusiveLength);
if (is_file($filePath)) {
$zipFile->addFile($filePath, $localPath);
$progress->advance();
} elseif (is_dir($filePath)) {
// Add sub-directory.
$zipFile->addEmptyDir($localPath);
self::folderToZip($filePath, $zipFile, $exclusiveLength, $progress);
switch ($args['type']) {
case 'message':
$this->output->writeln($args['message']);
break;
case 'progress':
if ($args['complete']) {
$this->progress->finish();
} else {
$this->progress->advance();
}
}
break;
}
closedir($handle);
}
}

View File

@@ -66,7 +66,6 @@ class CleanCommand extends Command
'vendor/filp/whoops/.scrutinizer.yml',
'vendor/filp/whoops/.travis.yml',
'vendor/filp/whoops/phpunit.xml.dist',
'vendor/filp/whoops/src/deprecated',
'vendor/gregwar/image/Gregwar/Image/composer.json',
'vendor/gregwar/image/Gregwar/Image/phpunit.xml',
'vendor/gregwar/image/Gregwar/Image/.gitignore',

View File

@@ -0,0 +1,80 @@
<?php
namespace Grav\Console\Cli;
use Grav\Console\ConsoleTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
/**
* Class ComposerCommand
* @package Grav\Console\Cli
*/
class ComposerCommand extends Command
{
use ConsoleTrait;
/**
* @var
*/
protected $config;
/**
* @var
*/
protected $local_config;
/**
* @var
*/
protected $destination;
/**
* @var
*/
protected $user_path;
/**
*
*/
protected function configure()
{
$this
->setName("composer")
->addOption(
'install',
'i',
InputOption::VALUE_NONE,
'install the dependencies'
)
->addOption(
'update',
'u',
InputOption::VALUE_NONE,
'update the dependencies'
)
->setDescription("Updates the composer vendordependencies needed by Grav.")
->setHelp('The <info>composer</info> command updates the composer vendordependencies needed by Grav');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int|null|void
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$action = 'update';
if ($input->getOption('install')) {
$action = 'install';
}
// Updates composer first
$output->writeln("\nInstalling vendor dependencies");
$output->writeln($this->composerUpdate(GRAV_ROOT, $action));
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Grav\Console\Cli;
use Grav\Console\ConsoleTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
@@ -15,7 +16,7 @@ use Symfony\Component\Yaml\Yaml;
*/
class InstallCommand extends Command
{
use ConsoleTrait;
/**
* @var
*/
@@ -98,7 +99,7 @@ class InstallCommand extends Command
if (!$input->getOption('symlink')) {
// Updates composer first
$output->writeln("\nInstalling vendor dependencies");
$output->writeln(system('php bin/composer.phar --working-dir="'.$this->destination.'" --no-interaction update'));
$output->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
$this->gitclone($output);
} else {

View File

@@ -2,7 +2,6 @@
namespace Grav\Console\Cli;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Utils;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
@@ -20,6 +19,7 @@ class SandboxCommand extends Command
* @var array
*/
protected $directories = array(
'/backup',
'/cache',
'/logs',
'/images',
@@ -189,7 +189,7 @@ class SandboxCommand extends Command
$to = $this->destination . $target;
$this->output->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
Utils::rcopy($from, $to);
Folder::rcopy($from, $to);
}
}
@@ -269,7 +269,7 @@ class SandboxCommand extends Command
if (count($pages_files) == 0) {
$destination = $this->source . '/user/pages';
Utils::rcopy($destination, $pages_dir);
Folder::rcopy($destination, $pages_dir);
$this->output->writeln(' <cyan>' . $destination . '</cyan> <comment>-></comment> Created');
}

View File

@@ -83,6 +83,11 @@ trait ConsoleTrait
}
}
public function composerUpdate($path, $action = 'install')
{
return system('php bin/composer.phar --working-dir="'.$path.'" --no-interaction --no-dev --prefer-dist -o '. $action);
}
/**
* @param array $all
*

View File

@@ -255,7 +255,7 @@ class InstallCommand extends Command
// Confirmation received, copy over the data
$this->output->writeln(" |- Installing demo content... <green>ok</green> ");
Utils::rcopy($demo_dir, $dest_dir);
Folder::rcopy($demo_dir, $dest_dir);
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
}
@@ -359,8 +359,6 @@ class InstallCommand extends Command
{
$matches = $this->getGitRegexMatches($package);
$to = $this->destination . DS . $package->install_path;
$this->output->writeln("Preparing to Git clone <cyan>" . $package->name . "</cyan> from " . $matches[0]);
$this->output->write(" |- Checking destination... ");

View File

@@ -1,51 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<defaultDocument>
<files>
<remove value="index.php" />
<add value="index.php" />
</files>
</defaultDocument>
<rewrite>
<rules>
<rule name="request_filename" stopProcessing="true">
<match url="." ignoreCase="false" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
<rule name="user_accounts" stopProcessing="true">
<match url="^user/accounts/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="user_config" stopProcessing="true">
<match url="^user/config/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="user_error_redirect" stopProcessing="true">
<match url="^user/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="cache" stopProcessing="true">
<match url="^cache/(.*)" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="bin" stopProcessing="true">
<match url="^bin/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="system" stopProcessing="true">
<match url="^system/(.*)\.(txt|md|html|yaml|php|twig|sh|bat)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="vendor" stopProcessing="true">
<match url="^vendor/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<defaultDocument>
<files>
<remove value="index.php" />
<add value="index.php" />
</files>
</defaultDocument>
<rewrite>
<rules>
<rule name="request_filename" stopProcessing="true">
<match url="." ignoreCase="false" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
<rule name="user_accounts" stopProcessing="true">
<match url="^user/accounts/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="user_config" stopProcessing="true">
<match url="^user/config/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="user_error_redirect" stopProcessing="true">
<match url="^user/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="cache" stopProcessing="true">
<match url="^cache/(.*)" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="bin" stopProcessing="true">
<match url="^bin/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="backup" stopProcessing="true">
<match url="^backup/(.*)" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="system" stopProcessing="true">
<match url="^system/(.*)\.(txt|md|html|yaml|php|twig|sh|bat)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
<rule name="vendor" stopProcessing="true">
<match url="^vendor/(.*)$" ignoreCase="false" />
<action type="Redirect" url="error" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>