mirror of
https://github.com/getgrav/grav.git
synced 2026-03-05 03:51:50 +01:00
Merge branch 'release/0.9.25'
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,3 +1,19 @@
|
||||
# v0.9.25
|
||||
## 04/24/2015
|
||||
|
||||
1. [](#new)
|
||||
* Added support for E-Tag, Last-Modified, Cache-Control and Page-based expires headers
|
||||
2. [](#improved)
|
||||
* Refactored media image handling to make it more flexible and support absolute paths
|
||||
* Refactored page modification check process to make it faster
|
||||
* User account improvements in preparation for Admin plugin
|
||||
* Protect against timing attacks
|
||||
* Reset default system expires time to 0 seconds (can override if you need to)
|
||||
3. [](#bugfix)
|
||||
* Fix issues with spaces in webroot when using `bin/grav install`
|
||||
* Fix for spaces in relative directory
|
||||
* Bug fix in collection filtering
|
||||
|
||||
# v0.9.24
|
||||
## 04/15/2015
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad) [](https://gitter.im/getgrav/grav?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
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 principals 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.
|
||||
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.
|
||||
|
||||
The underlying architecture of Grav has been designed to use well-established and _best-in-class_ technologies, where applicable, to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
|
||||
The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies, to ensure that Grav is simple to use and easy to extend. Some of these key technologies include:
|
||||
|
||||
* [Twig Templating](http://twig.sensiolabs.org/): for powerful control of the user interface
|
||||
* [Markdown](http://en.wikipedia.org/wiki/Markdown): for easy content creation
|
||||
|
||||
@@ -21,15 +21,8 @@
|
||||
"mrclay/minify": "dev-master",
|
||||
"donatj/phpuseragentparser": "dev-master",
|
||||
"pimple/pimple": "~3.0",
|
||||
"rockettheme/toolbox": "dev-develop"
|
||||
"rockettheme/toolbox": "1.0.*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"no-api": true,
|
||||
"url": "https://github.com/rockettheme/toolbox"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grav\\": "system/src/Grav"
|
||||
|
||||
@@ -31,7 +31,7 @@ pages:
|
||||
'>': 'gt'
|
||||
'<': 'lt'
|
||||
types: 'txt|xml|html|json|rss|atom' # Pipe separated list of valid page types
|
||||
expires: 604800 # Page expires time in seconds (default 7 days)
|
||||
expires: 0 # Page expires time in seconds (604800 seconds = 7 days)
|
||||
|
||||
cache:
|
||||
enabled: true # Set to true to enable caching
|
||||
@@ -78,3 +78,6 @@ images:
|
||||
|
||||
media:
|
||||
enable_media_timestamp: false # Enable media timetsamps
|
||||
|
||||
security:
|
||||
default_hash: $2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Some standard defines
|
||||
define('GRAV', true);
|
||||
define('GRAV_VERSION', '0.9.24');
|
||||
define('GRAV_VERSION', '0.9.25');
|
||||
define('DS', '/');
|
||||
|
||||
// Directories and Paths
|
||||
|
||||
@@ -42,17 +42,16 @@ abstract class Folder
|
||||
*/
|
||||
public static function lastModifiedFile($path)
|
||||
{
|
||||
// pipe separated list of extensions to search for changes with
|
||||
$extensions = 'md|yaml';
|
||||
$last_modified = 0;
|
||||
|
||||
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$filterItr = new RecursiveFileFilterIterator($dirItr);
|
||||
$itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST);
|
||||
$itr = new \RegexIterator($itrItr, '/^.+\.'.$extensions.'$/i');
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
foreach ($itr as $file) {
|
||||
if ($file->isDir()) {
|
||||
continue;
|
||||
}
|
||||
foreach ($itr as $filepath => $file) {
|
||||
$file_modified = $file->getMTime();
|
||||
if ($file_modified > $last_modified) {
|
||||
$last_modified = $file_modified;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
namespace Grav\Common\Filesystem;
|
||||
|
||||
class RecursiveFileFilterIterator extends \RecursiveFilterIterator
|
||||
{
|
||||
public static $FILTERS = ['.DS_Store'];
|
||||
|
||||
public function accept()
|
||||
{
|
||||
// Ensure any filtered file names are skipped
|
||||
return !in_array($this->current()->getFilename(), self::$FILTERS, true);
|
||||
}
|
||||
}
|
||||
@@ -296,9 +296,27 @@ class Grav extends Container
|
||||
public function header()
|
||||
{
|
||||
$extension = $this['uri']->extension();
|
||||
|
||||
/** @var Page $page */
|
||||
$page = $this['page'];
|
||||
|
||||
header('Content-type: ' . $this->mime($extension));
|
||||
|
||||
header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + $this['config']->get('system.pages.expires')));
|
||||
// Calculate Expires Headers if set to > 0
|
||||
$expires = $page->expires();
|
||||
|
||||
if ($expires > 0) {
|
||||
$expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
|
||||
header('Cache-Control: max-age=' . $expires_date);
|
||||
header('Expires: '. $expires_date);
|
||||
}
|
||||
|
||||
// Set the last modified time
|
||||
$last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
|
||||
header('Last-Modified: ' . $last_modified_date);
|
||||
|
||||
// Calculate a Hash based on the raw file
|
||||
header('ETag: ' . md5($page->raw().$page->modified()));
|
||||
|
||||
// Set debugger data in headers
|
||||
if (!($extension === null || $extension == 'html')) {
|
||||
|
||||
@@ -5,9 +5,9 @@ class Parsedown extends \Parsedown
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
public function __construct($page)
|
||||
public function __construct($page, $defaults)
|
||||
{
|
||||
$this->init($page);
|
||||
$this->init($page, $defaults);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ class ParsedownExtra extends \ParsedownExtra
|
||||
{
|
||||
use ParsedownGravTrait;
|
||||
|
||||
public function __construct($page)
|
||||
public function __construct($page, $defaults)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->init($page);
|
||||
$this->init($page, $defaults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Grav\Common\Debugger;
|
||||
use Grav\Common\GravTrait;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\Utils;
|
||||
|
||||
/**
|
||||
* A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
|
||||
@@ -25,8 +26,9 @@ trait ParsedownGravTrait
|
||||
* Initialiazation function to setup key variables needed by the MarkdownGravLinkTrait
|
||||
*
|
||||
* @param $page
|
||||
* @param $defaults
|
||||
*/
|
||||
protected function init($page)
|
||||
protected function init($page, $defaults)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pages = self::getGrav()['pages'];
|
||||
@@ -35,7 +37,9 @@ trait ParsedownGravTrait
|
||||
$this->pages_dir = self::getGrav()['locator']->findResource('page://');
|
||||
$this->special_chars = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
|
||||
|
||||
$defaults = self::getGrav()['config']->get('system.pages.markdown');
|
||||
if ($defaults == null) {
|
||||
$defaults = self::getGrav()['config']->get('system.pages.markdown');
|
||||
}
|
||||
|
||||
$this->setBreaksEnabled($defaults['auto_line_breaks']);
|
||||
$this->setUrlsLinked($defaults['auto_url_links']);
|
||||
@@ -116,7 +120,6 @@ trait ParsedownGravTrait
|
||||
|
||||
// if this is an image
|
||||
if (isset($excerpt['element']['attributes']['src'])) {
|
||||
|
||||
$alt = $excerpt['element']['attributes']['alt'] ?: '';
|
||||
$title = $excerpt['element']['attributes']['title'] ?: '';
|
||||
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
|
||||
@@ -124,10 +127,9 @@ trait ParsedownGravTrait
|
||||
//get the url and parse it
|
||||
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
|
||||
|
||||
$path_parts = pathinfo($url['path']);
|
||||
|
||||
// if there is no host set but there is a path, the file is local
|
||||
if (!isset($url['host']) && isset($url['path'])) {
|
||||
$path_parts = pathinfo($url['path']);
|
||||
|
||||
// get the local path to page media if possible
|
||||
if ($path_parts['dirname'] == $this->page->url()) {
|
||||
@@ -136,7 +138,6 @@ trait ParsedownGravTrait
|
||||
$media = $this->page->media();
|
||||
|
||||
} else {
|
||||
|
||||
// see if this is an external page to this one
|
||||
$page_route = str_replace($this->base_url, '', $path_parts['dirname']);
|
||||
|
||||
@@ -219,37 +220,58 @@ trait ParsedownGravTrait
|
||||
protected function convertUrl($markdown_url)
|
||||
{
|
||||
// if absolute and starts with a base_url move on
|
||||
if ($this->base_url != '' && strpos($markdown_url, $this->base_url) === 0) {
|
||||
if ($this->base_url != '' && Utils::startsWith($markdown_url, $this->base_url)) {
|
||||
return $markdown_url;
|
||||
// if contains only a fragment
|
||||
} elseif (Utils::startsWith($markdown_url, '#')) {
|
||||
return $markdown_url;
|
||||
// if its absolute and starts with /
|
||||
} elseif (strpos($markdown_url, '/') === 0) {
|
||||
return $this->base_url . $markdown_url;
|
||||
} else {
|
||||
$relative_path = $this->base_url . $this->page->route();
|
||||
$real_path = $this->page->path() . '/' . parse_url($markdown_url, PHP_URL_PATH);
|
||||
|
||||
// strip numeric order from markdown path
|
||||
if (($real_path)) {
|
||||
$markdown_url = preg_replace('/^([\d]+\.)/', '', preg_replace('/\/([\d]+\.)/', '/', trim(preg_replace('/[^\/]+(\.md$)/', '', $markdown_url), '/')));
|
||||
$target = null;
|
||||
// see if page is relative to this or absolute
|
||||
if (Utils::startsWith($markdown_url, '/')) {
|
||||
$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);
|
||||
}
|
||||
|
||||
// else its a relative path already
|
||||
$newpath = array();
|
||||
$paths = explode('/', $markdown_url);
|
||||
// if this file exits, get the page and work with that
|
||||
if ($normalized_path) {
|
||||
$url_bits = parse_url($normalized_path);
|
||||
$full_path = $url_bits['path'];
|
||||
|
||||
// remove the updirectory references (..)
|
||||
foreach ($paths as $path) {
|
||||
if ($path == '..') {
|
||||
$relative_path = dirname($relative_path);
|
||||
} else {
|
||||
$newpath[] = $path;
|
||||
if ($full_path && file_exists($full_path)) {
|
||||
$path_info = pathinfo($full_path);
|
||||
$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 {
|
||||
$page_path = $full_path;
|
||||
}
|
||||
|
||||
// get page instances and try to find one that fits
|
||||
$instances = $this->pages->instances();
|
||||
if (isset($instances[$page_path])) {
|
||||
$target = $instances[$page_path];
|
||||
$url_bits['path'] = $this->base_url . $target->route() . $filename;
|
||||
return Uri::buildUrl($url_bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build the new url
|
||||
$new_url = rtrim($relative_path, '/') . '/' . implode('/', $newpath);
|
||||
return $normalized_url;
|
||||
}
|
||||
|
||||
return $new_url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,12 +361,13 @@ class Collection extends Iterator
|
||||
{
|
||||
$routable = [];
|
||||
|
||||
foreach (array_keys($this->items) as $path => $slug) {
|
||||
foreach ($this->items as $path => $slug) {
|
||||
$page = $this->pages->get($path);
|
||||
if ($page->routable()) {
|
||||
$routable[$path] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items = $routable;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ trait ParsedownHtmlTrait
|
||||
$element = $this->parsedownElement($title, $alt, $class, $reset);
|
||||
|
||||
if (!$this->parsedown) {
|
||||
$this->parsedown = new Parsedown(null);
|
||||
$this->parsedown = new Parsedown(null, null);
|
||||
}
|
||||
|
||||
return $this->parsedown->elementToHtml($element);
|
||||
|
||||
@@ -47,6 +47,7 @@ class Page
|
||||
|
||||
protected $parent;
|
||||
protected $template;
|
||||
protected $expires;
|
||||
protected $visible;
|
||||
protected $published;
|
||||
protected $publish_date;
|
||||
@@ -273,6 +274,10 @@ class Page
|
||||
if (isset($this->header->unpublish_date)) {
|
||||
$this->unpublish_date = strtotime($this->header->unpublish_date);
|
||||
}
|
||||
if (isset($this->header->expires)) {
|
||||
$this->expires = intval($this->header->expires);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this->header;
|
||||
@@ -442,9 +447,9 @@ class Page
|
||||
|
||||
// Initialize the preferred variant of Parsedown
|
||||
if ($defaults['extra']) {
|
||||
$parsedown = new ParsedownExtra($this);
|
||||
$parsedown = new ParsedownExtra($this, $defaults);
|
||||
} else {
|
||||
$parsedown = new Parsedown($this);
|
||||
$parsedown = new Parsedown($this, $defaults);
|
||||
}
|
||||
|
||||
$this->content = $parsedown->text($this->content);
|
||||
@@ -781,6 +786,20 @@ class Page
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the expires field. If not set will return the default
|
||||
*
|
||||
* @param string $var The name of this page.
|
||||
* @return string The name of this page.
|
||||
*/
|
||||
public function expires($var = null)
|
||||
{
|
||||
if ($var !== null) {
|
||||
$this->expires = $var;
|
||||
}
|
||||
return empty($this->expires) ? self::getGrav()['config']->get('system.pages.expires') : $this->expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name
|
||||
*
|
||||
|
||||
@@ -331,6 +331,28 @@ class Pages
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages
|
||||
*
|
||||
* @param \Grav\Common\Page\Page $current
|
||||
* @return \Grav\Common\Page\Collection
|
||||
*/
|
||||
public function all(Page $current = null)
|
||||
{
|
||||
$all = new Collection();
|
||||
$current = $current ?: $this->root();
|
||||
|
||||
if ($current->routable()) {
|
||||
$all[$current->path()] = [ 'slug' => $current->slug() ];
|
||||
}
|
||||
|
||||
foreach ($current->children() as $next) {
|
||||
$all->append($this->all($next));
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of route/title of all pages.
|
||||
*
|
||||
|
||||
@@ -337,9 +337,9 @@ class TwigExtension extends \Twig_Extension
|
||||
|
||||
// Initialize the preferred variant of Parsedown
|
||||
if ($defaults['extra']) {
|
||||
$parsedown = new ParsedownExtra($page);
|
||||
$parsedown = new ParsedownExtra($page, $defaults);
|
||||
} else {
|
||||
$parsedown = new Parsedown($page);
|
||||
$parsedown = new Parsedown($page, $defaults);
|
||||
}
|
||||
|
||||
$string = $parsedown->text($string);
|
||||
|
||||
@@ -34,8 +34,7 @@ class Uri
|
||||
$port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
|
||||
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
|
||||
|
||||
$root_path = rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/');
|
||||
|
||||
$root_path = str_replace(' ', '%20', rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/'));
|
||||
|
||||
if (isset($_SERVER['HTTPS'])) {
|
||||
$base = (@$_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
||||
|
||||
@@ -13,11 +13,22 @@ abstract class Authentication
|
||||
* Create password hash from plaintext password.
|
||||
*
|
||||
* @param string $password Plaintext password.
|
||||
* @throws \RuntimeException
|
||||
* @return string|bool
|
||||
*/
|
||||
public static function create($password)
|
||||
{
|
||||
return password_hash($password, PASSWORD_DEFAULT);
|
||||
if (!$password) {
|
||||
throw new \RuntimeException('Password hashing failed: no password provided.');
|
||||
}
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
if (!$hash) {
|
||||
throw new \RuntimeException('Password hashing failed: internal error.');
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,13 +40,8 @@ abstract class Authentication
|
||||
*/
|
||||
public static function verify($password, $hash)
|
||||
{
|
||||
// Always accept plaintext passwords (needs an update).
|
||||
if ($password && $password == $hash) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Fail if hash doesn't match.
|
||||
if (!$password || !password_verify($password, $hash)) {
|
||||
// Fail if hash doesn't match
|
||||
if (!$password || !$hash || !password_verify($password, $hash)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,11 +53,36 @@ class User extends Data
|
||||
*/
|
||||
public function authenticate($password)
|
||||
{
|
||||
$result = Authentication::verify($password, $this->password);
|
||||
$save = false;
|
||||
|
||||
// Plain-text is still stored
|
||||
if ($this->password) {
|
||||
|
||||
if ($password !== $this->password) {
|
||||
// Plain-text passwords do not match, we know we should fail but execute
|
||||
// verify to protect us from timing attacks and return false regardless of
|
||||
// the result
|
||||
Authentication::verify($password, self::getGrav()['config']->get('system.security.default_hash'));
|
||||
return false;
|
||||
} else {
|
||||
// Plain-text does match, we can update the hash and proceed
|
||||
$save = true;
|
||||
|
||||
$this->hashed_password = Authentication::create($this->password);
|
||||
unset($this->password);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$result = Authentication::verify($password, $this->hashed_password);
|
||||
|
||||
// Password needs to be updated, save the file.
|
||||
if ($result == 2) {
|
||||
$this->password = Authentication::create($password);
|
||||
$save = true;
|
||||
$this->hashed_password = Authentication::create($password);
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,6 @@ abstract class Utils
|
||||
* Return the mimetype based on filename
|
||||
*
|
||||
* @param $extension Extension of file (eg .txt)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getMimeType($extension)
|
||||
@@ -396,4 +395,29 @@ abstract class Utils
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path by processing relative `.` and `..` syntax and merging path
|
||||
*
|
||||
* @param $path
|
||||
* @return string
|
||||
*/
|
||||
public static function normalizePath($path)
|
||||
{
|
||||
$root = ($path[0] === '/') ? '/' : '';
|
||||
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
$ret = array();
|
||||
foreach ($segments as $segment) {
|
||||
if (($segment == '.') || empty($segment)) {
|
||||
continue;
|
||||
}
|
||||
if ($segment == '..') {
|
||||
array_pop($ret);
|
||||
} else {
|
||||
array_push($ret, $segment);
|
||||
}
|
||||
}
|
||||
return $root . implode('/', $ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class InstallCommand extends Command
|
||||
foreach ($this->config['git'] as $repo => $data) {
|
||||
$path = $this->destination . DS . $data['path'];
|
||||
if (!file_exists($path)) {
|
||||
exec('cd ' . $this->destination . ' && git clone -b ' . $data['branch'] . ' ' . $data['url'] . ' ' . $data['path']);
|
||||
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' ' . $data['url'] . ' ' . $data['path']);
|
||||
$output->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
|
||||
$output->writeln('');
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user