Compare commits

...

52 Commits

Author SHA1 Message Date
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
f5fe524e6c Added extension whitelist for image uploads
- A continuation of the security issues addressed in v0.25.3
2019-03-21 19:43:15 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
37b91b6b0e Hardened image file validation by removing custom validation
- Added test to check PHP files cannot be uploaded as an image.
2019-03-20 23:59:55 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
00703fa817 Merge branch 'dfanara-feature-ldap-attributes' 2019-03-10 10:55:36 +00:00
Dan Brown
44c537de1a Performed some LDAP service/test cleanup 2019-03-10 10:54:19 +00:00
Dan Brown
6bccf0e64a Merge branch 'feature-ldap-attributes' of git://github.com/dfanara/BookStack into dfanara-feature-ldap-attributes 2019-03-10 10:31:09 +00:00
Dan Brown
042a6f9760 Updated shelf menu item to show on custom permission
- Extended new 'userCanOnAny' helper to take a entity class for
filtering.

Closes #1201
2019-03-09 21:15:45 +00:00
Dan Brown
04287745e4 Merge branch 'mark-james-Copy-For-View-Only' 2019-03-09 16:52:47 +00:00
Dan Brown
5c9b528517 Abstracted userCanCreatePage helper to work for any permisison
- Added test to cover scenario where someone with create-own permission
would want to copy a viewable item into a container entity that they
own.
2019-03-09 16:50:22 +00:00
Dan Brown
6be2d3f28c Merge branch 'Copy-For-View-Only' of git://github.com/mark-james/BookStack into mark-james-Copy-For-View-Only 2019-03-09 16:12:12 +00:00
Daniel Fanara
6d20bdc1fb Preserve original display_name_attribute configuration values. 2019-03-09 01:13:30 -05:00
Daniel Fanara
502ea608bf Issue #1306 - Unit Tests for LdapService Changes 2019-03-09 01:08:49 -05:00
Daniel Fanara
55b07c7076 Issue #1306 - Specify display name attribute from LDAP 2019-03-08 23:55:11 -05:00
Dan Brown
33e999909f Merge branch 'fix-1186' 2019-03-08 22:57:57 +00:00
Dan Brown
6be95cd2ac Re-centered dropzone error arrow 2019-03-08 22:57:24 +00:00
Dan Brown
f467185e86 Merge branch 'master' into fix-1186 2019-03-08 22:47:31 +00:00
Dan Brown
646fd822c5 Updated redis config logic, Now takes a password
- Previous config did not use multiple servers in any way.
- Cluster will now be created automatically if multiple servers given.
- Removed REDIS_CLUSTER option.

Closes #1283
2019-03-08 22:42:48 +00:00
Dan Brown
d96baf2d4a Set 'uploaded_to' parameters for editor-pasted/dragged images
Allows image-listing permission system to work as intended.
Fixes #1287
2019-03-08 21:32:31 +00:00
Dan Brown
1c312906bc Added a configurable upload size limit
Closes #1293
2019-03-08 21:06:37 +00:00
Dan Brown
9126c87f2b Merge pull request #1272 from Xiphoseer/patch-1
Add german translations for shelves
2019-03-07 22:14:35 +00:00
Dan Brown
579d98a908 Merge pull request #1314 from maantje/patch-2
Update Dutch password_hint translation to correspond with validation
2019-03-07 21:53:08 +00:00
Dan Brown
98a4359198 Updated user language select to use correct default
- Updated localisation system to take note of system defaul locale
before replacing the current locale
Fixes #1316
2019-03-07 21:09:23 +00:00
Jamie Schouten
6f710225b5 Update Dutch password_hint translation to correspond with validation rule
At the moment the translation says ```Minimaal 5 tekens``` which means your password should be at least 5 characters long. But a 5 character long password is not allowed by the validator. 

I think this was a translation error from the English one where it says ```Must be over 5 characters```. To make the Dutch translation correct the Dutch translation should be changed to ```Minimaal 6 tekens```.

```
    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6',
        ]);
    }
```
2019-03-06 17:10:15 +01:00
Dan Brown
b273b9d6d0 Improved alignment classes used by WYSIWYG editor
- Fixed table cells being floated, Fixes #1284.
- Made it possible to easily center linked images.
2019-03-02 09:08:01 +00:00
Dan Brown
e471d0c52a Added lua to code languages
Closes #1223
2019-03-02 08:52:14 +00:00
Dan Brown
0a431e3223 Merge pull request #1263 from christophert/add-powershellmarkup
Add Powershell Code Markup
2019-03-02 08:45:08 +00:00
Xiphoseer
058cc2cbd6 Update entities.php 2019-02-12 12:30:43 +01:00
Xiphoseer
edd98c00e5 Update entities.php 2019-02-12 12:30:12 +01:00
Xiphoseer
19bb11a1c9 Update entities.php
Add informal german shelve localisations
2019-02-12 12:28:48 +01:00
Xiphoseer
9511d10ec8 Update entities.php
Add german shelve localizations
2019-02-12 12:24:01 +01:00
Christopher Tran
77d3bd31a6 add powershell code block link 2019-02-06 01:06:59 -05:00
Dan Brown
56004abdf4 Added high-level release and roadmap info to readme
Closes #1259
2019-02-06 00:09:39 +00:00
Dan Brown
df6f6e2d77 Merge branch 'master' of github.com:BookStackApp/BookStack 2019-02-04 19:57:43 +00:00
Dan Brown
ba1b3fc181 Made some readme tweaks 2019-02-04 19:57:21 +00:00
abijeet
9dba9ca178 Fixes tooltip on the image manager.
Fixes #1186
2019-01-27 19:43:31 +05:30
Abijeet Patro
8a4a81629f Merge pull request #1237 from BookStackApp/phpcs-fixes
PHPCS related fixes.
2019-01-27 16:04:30 +05:30
abijeet
5ef0992d5b PHPCS related fixes. 2019-01-27 15:59:23 +05:30
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
12be7d0086 Added extra s3 config parameters for use s3-like service compatibility
For #1192 and #1195
2019-01-20 15:23:49 +00:00
Dan Brown
ba0af9214e Updated socialite to work around google+ API shutdown
Fixes #1190
Will require docs update
2019-01-20 14:58:06 +00:00
Dan Brown
36424a24b5 Added ability for date format strings to be localized by back-end
Requires the locale to be installed on the system-side.
Closes #1214
2019-01-19 12:11:18 +00:00
Dan Brown
a70ee9664a Fixed firefox page print view and removed comments from prints
Closes #1211
2019-01-19 11:33:27 +00:00
Dan Brown
156c0a88e9 Updated sidebar to prevent rubber-banding with comments disabled
Fixes #1218
2019-01-19 11:10:46 +00:00
Mark James
19770d2792 Use joint_permissions to determine is a user has an available page or chapter to copy. 2019-01-02 16:55:28 +11:00
Mark James
99c6d70c51 Initial updates to allow for page copy when the user can read the page but can't update it. 2018-12-31 17:01:49 +11:00
mark-james
0830521e60 Merge pull request #1 from BookStackApp/master
Update From Bookstack
2018-12-31 09:00:19 +11:00
53 changed files with 640 additions and 223 deletions

View File

@@ -75,6 +75,12 @@ CACHE_PREFIX=bookstack
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Redis server configuration
# This follows the following format: HOST:PORT:DATABASE
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
# For multiple servers separate with a comma. These will be clustered.
REDIS_SERVERS=127.0.0.1:6379:0
# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
@@ -90,6 +96,10 @@ STORAGE_S3_SECRET=your-s3-secret
STORAGE_S3_BUCKET=s3-bucket-name
STORAGE_S3_REGION=s3-bucket-region
# S3 endpoint to use for storage calls
# Only set this if using a non-Amazon s3-compatible service such as Minio
STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
# Storage URL prefix
# Used as a base for any generated image urls.
# An s3-format URL will be generated if not set.
@@ -167,6 +177,7 @@ LDAP_USER_FILTER=false
LDAP_VERSION=false
LDAP_TLS_INSECURE=false
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_FOLLOW_REFERRALS=true
# LDAP group sync configuration

View File

@@ -80,20 +80,40 @@ class LdapService
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
$displayNameAttr = $this->config['display_name_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
if ($user === null) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
];
}
/**
* Get a property from an LDAP user response fetch.
* Handles properties potentially being part of an array.
* @param array $userDetails
* @param string $propertyKey
* @param $defaultValue
* @return mixed
*/
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
{
if (isset($userDetails[$propertyKey])) {
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
}
return $defaultValue;
}
/**
* @param Authenticatable $user
* @param string $username
@@ -176,8 +196,8 @@ class LdapService
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
* per handle.
*/
if($this->config['tls_insecure']) {
$this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
if ($this->config['tls_insecure']) {
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);

View File

@@ -190,10 +190,10 @@ class PermissionService
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
/**
@@ -556,6 +556,39 @@ class PermissionService
return $q;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
{
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
$permissionQuery = $this->db->table('joint_permissions')
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
->where(function ($query) use ($userId) {
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
});
}) ;
if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
@@ -612,13 +645,13 @@ class PermissionService
$entities = $this->entityProvider;
$pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);

View File

@@ -84,6 +84,4 @@ class EntityProvider
$type = strtolower($type);
return $this->all()[$type];
}
}
}

View File

@@ -505,4 +505,4 @@ class PageRepo extends EntityRepo
return $this->publishPageDraft($copyPage, $pageData);
}
}
}

View File

@@ -2,4 +2,6 @@
use Exception;
class HttpFetchException extends Exception {}
class HttpFetchException extends Exception
{
}

View File

@@ -1,3 +1,5 @@
<?php namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException {}
class UserUpdateException extends NotifyException
{
}

View File

@@ -119,7 +119,7 @@ class ImageController extends Controller
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'is_image'
'file' => 'image_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
]);
if (!$this->imageRepo->isValidType($type)) {
@@ -135,7 +135,6 @@ class ImageController extends Controller
return response($e->getMessage(), 500);
}
return response()->json($image);
}

View File

@@ -643,7 +643,7 @@ class PageController extends Controller
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
'book' => $page->book,
@@ -662,7 +662,7 @@ class PageController extends Controller
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {

View File

@@ -7,8 +7,40 @@ use Illuminate\Http\Request;
class Localization
{
/**
* Array of right-to-left locales
* @var array
*/
protected $rtlLocales = ['ar'];
/**
* Map of BookStack locale names to best-estimate system locale names.
* @var array
*/
protected $localeMap = [
'ar' => 'ar',
'de' => 'de_DE',
'de_informal' => 'de_DE',
'en' => 'en_GB',
'es' => 'es_ES',
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'it' => 'it_IT',
'ja' => 'ja',
'kr' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt_BR' => 'pt_BR',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
'sv' => 'sv_SE',
'uk' => 'uk_UA',
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
];
/**
* Handle an incoming request.
*
@@ -19,6 +51,7 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
config()->set('app.default_locale', $defaultLang);
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
@@ -33,6 +66,7 @@ class Localization
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
return $next($request);
}
@@ -53,4 +87,18 @@ class Localization
}
return $default;
}
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
* @param string $locale
*/
protected function setSystemDateLocale(string $locale)
{
$systemLocale = $this->localeMap[$locale] ?? $locale;
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
}
}
}

View File

@@ -31,5 +31,4 @@ class MailNotification extends Notification implements ShouldQueue
'text' => 'vendor.notifications.email-plain'
]);
}
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Notifications;
class ResetPassword extends MailNotification
{
/**

View File

@@ -8,6 +8,7 @@ use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@@ -22,11 +23,12 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
});
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
@@ -29,4 +28,4 @@ class TranslationServiceProvider extends \Illuminate\Translation\TranslationServ
return $trans;
});
}
}
}

View File

@@ -41,6 +41,7 @@ class SettingService
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
@@ -70,5 +69,4 @@ class Translator extends \Illuminate\Translation\Translator
{
return $this->baseLocaleMap[$locale] ?? null;
}
}
}

View File

@@ -30,5 +30,4 @@ class HttpFetcher
return $data;
}
}
}

View File

@@ -1,5 +1,7 @@
<?php
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Ownable;
/**
@@ -50,21 +52,34 @@ function signedInUser()
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
* that particular item.
* @param $permission
* @param string $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan($permission, Ownable $ownable = null)
function userCan(string $permission, Ownable $ownable = null)
{
if ($ownable === null) {
return user() && user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
$permissionService = app(PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
/**
* Check if the current user has the given permission
* on any item in the system.
* @param string $permission
* @param string|null $entityClass
* @return bool
*/
function userCanOnAny(string $permission, string $entityClass = null)
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
}
/**
* Helper to access system settings.
* @param $key

View File

@@ -16,7 +16,7 @@
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
"laravel/socialite": "^3.0",
"laravel/socialite": "3.0.x-dev",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",

93
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "06219a5c2419ca23ec2924eb31f4ed16",
"content-hash": "0946a07729a7a1bfef9bac185a870afd",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.82.3",
"version": "3.86.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7"
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2018-12-21T22:21:50+00:00"
"time": "2019-01-18T21:10:44+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
@@ -1457,16 +1457,16 @@
},
{
"name": "laravel/socialite",
"version": "v3.2.0",
"version": "3.0.x-dev",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481"
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481",
"url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
"shasum": ""
},
"require": {
@@ -1516,7 +1516,7 @@
"laravel",
"oauth"
],
"time": "2018-10-18T03:39:04+00:00"
"time": "2018-12-21T14:06:32+00:00"
},
{
"name": "league/flysystem",
@@ -1891,16 +1891,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.36.1",
"version": "1.36.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983"
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"shasum": ""
},
"require": {
@@ -1945,7 +1945,7 @@
"datetime",
"time"
],
"time": "2018-11-22T18:23:02+00:00"
"time": "2018-12-28T10:07:33+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2555,20 +2555,20 @@
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.1",
"version": "v3.3.4",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
"shasum": ""
},
"require": {
"laravel/socialite": "~3.0",
"laravel/socialite": "~3.0|~4.0",
"php": "^5.6 || ^7.0"
},
"require-dev": {
@@ -2585,8 +2585,7 @@
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/",
"SocialiteProviders\\Manager\\Test\\": "tests/"
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2597,10 +2596,14 @@
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2017-11-20T08:42:57+00:00"
"time": "2019-01-16T07:58:54+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@@ -3664,16 +3667,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e"
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
"shasum": ""
},
"require": {
@@ -3710,7 +3713,7 @@
"env",
"environment"
],
"time": "2018-07-29T20:33:41+00:00"
"time": "2018-10-30T17:29:25+00:00"
}
],
"packages-dev": [
@@ -4423,23 +4426,23 @@
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.7",
"version": "5.2.8",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23"
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.1",
"friendsofphp/php-cs-fixer": "~2.2.20",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
@@ -4485,7 +4488,7 @@
"json",
"schema"
],
"time": "2018-02-14T22:26:30+00:00"
"time": "2019-01-14T23:55:14+00:00"
},
{
"name": "laravel/browser-kit-testing",
@@ -6265,20 +6268,21 @@
},
{
"name": "webmozart/assert",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@@ -6311,12 +6315,14 @@
"check",
"validate"
],
"time": "2018-01-29T19:49:41+00:00"
"time": "2018-12-25T11:19:39+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"laravel/socialite": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -6326,7 +6332,8 @@
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*"
"ext-gd": "*",
"ext-curl": "*"
},
"platform-dev": [],
"platform-overrides": {

View File

@@ -8,23 +8,39 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
// REDIS - Split out configuration into an array
// REDIS
// Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisServerKeys = ['host', 'port', 'database'];
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
$redisConfig = [];
$cluster = count($redisServers) > 1;
if ($cluster) {
$redisConfig['clusters'] = ['default' => []];
}
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
$serverConfig = [];
$configIndex = 0;
foreach ($redisDefaults as $configKey => $configDefault) {
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
$configIndex++;
}
if ($cluster) {
$redisConfig['clusters']['default'][] = $serverConfig;
} else {
$redisConfig['default'] = $serverConfig;
}
}
}
// MYSQL - Split out port from host if set
// MYSQL
// Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);

View File

@@ -49,6 +49,8 @@ return [
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
'region' => env('STORAGE_S3_REGION', 'your-region'),
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
'rackspace' => [

View File

@@ -141,6 +141,7 @@ return [
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),

12
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -2245,12 +2245,17 @@ ul.pagination {
float: left !important;
margin: 6px 12px 6px 0; }
.page-content .align-right {
float: right !important; }
text-align: right !important; }
.page-content img.align-right, .page-content table.align-right {
text-align: right;
float: right !important;
margin: 6px 0 6px 12px; }
.page-content .align-center {
text-align: center; }
.page-content img.align-center {
display: block; }
.page-content img.align-center, .page-content table.align-center {
margin-left: auto;
margin-right: auto; }
.page-content img {
max-width: 100%;
height: auto; }

View File

@@ -10,6 +10,12 @@ body {
.page-content {
margin: 0 auto; }
.flex-fill {
display: block; }
.flex.sidebar + .flex.content {
border-left: none; }
.print-hidden {
display: none; }
@@ -23,3 +29,6 @@ h2 {
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em; }
.comments-container {
display: none; }

View File

@@ -2876,7 +2876,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin-left: 1px;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #DDD; }
@@ -3178,8 +3177,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
font-size: 12px;
line-height: 1.2;
top: 88px;
left: -26px;
width: 148px;
left: -12px;
width: 160px;
background: #E84F4F;
padding: 6px;
color: white; }
@@ -3188,7 +3187,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
content: '';
position: absolute;
top: -6px;
left: 64px;
left: 44px;
width: 0;
height: 0;
border-left: 6px solid transparent;
@@ -3841,12 +3840,17 @@ ul.pagination {
float: left !important;
margin: 6px 12px 6px 0; }
.page-content .align-right {
float: right !important; }
text-align: right !important; }
.page-content img.align-right, .page-content table.align-right {
text-align: right;
float: right !important;
margin: 6px 0 6px 12px; }
.page-content .align-center {
text-align: center; }
.page-content img.align-center {
display: block; }
.page-content img.align-center, .page-content table.align-center {
margin-left: auto;
margin-right: auto; }
.page-content img {
max-width: 100%;
height: auto; }

View File

@@ -9,8 +9,7 @@ A platform for storing and organising information and documentation. General inf
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
* [Documentation](https://www.bookstackapp.com/docs)
* [Demo Instance](https://demo.bookstackapp.com)
* *Username: `admin@example.com`*
* *Password: `password`*
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
* [BookStack Blog](https://www.bookstackapp.com/blog)
## Project Definition
@@ -19,7 +18,30 @@ BookStack is an opinionated wiki system that provides a pleasant and simple out
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. Put simply, At the end of the day this is free software developed and maintained by people donating their own free time.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
## Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
- *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
- **Platform REST API**
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
- **Editor Alignment & Review**
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
- **Permission System Review**
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
## Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
## Development & Testing
@@ -85,15 +107,15 @@ PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr
### Pull Requests
Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
## Website, Docs & Blog
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## License
@@ -117,7 +139,6 @@ These are the great open-source projects used to help build BookStack:
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
* [Dompdf](https://github.com/barryvdh/laravel-dompdf)

View File

@@ -8,7 +8,11 @@ class MarkdownEditor {
constructor(elem) {
this.elem = elem;
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
const pageEditor = document.getElementById('page-editor');
this.pageId = pageEditor.getAttribute('page-id');
this.textDirection = pageEditor.getAttribute('text-direction');
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
@@ -98,7 +102,9 @@ class MarkdownEditor {
}
codeMirrorSetup() {
let cm = this.cm;
const cm = this.cm;
const context = this;
// Text direction
// cm.setOption('direction', this.textDirection);
cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
@@ -266,17 +272,18 @@ class MarkdownEditor {
}
// Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
let cursor = cm.getCursor();
const id = "image-" + Math.random().toString(16).slice(2);
const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
const selectedText = cm.getSelection();
const placeHolderText = `![${selectedText}](${placeholderImage})`;
const cursor = cm.getCursor();
cm.replaceSelection(placeHolderText);
cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
const remoteFilename = "image-" + Date.now() + "." + ext;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', context.pageId);
window.$http.post('/images/gallery/upload', formData).then(resp => {
const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
@@ -302,7 +309,7 @@ class MarkdownEditor {
}
actionInsertImage() {
let cursorPos = this.cm.getCursor('from');
const cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = this.cm.getSelection();
let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
@@ -313,7 +320,7 @@ class MarkdownEditor {
}
actionShowImageManager() {
let cursorPos = this.cm.getCursor('from');
const cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
this.insertDrawing(image, cursorPos);
}, 'drawio');
@@ -321,7 +328,7 @@ class MarkdownEditor {
// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
let cursorPos = this.cm.getCursor('from');
const cursorPos = this.cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = this.cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
@@ -357,7 +364,7 @@ class MarkdownEditor {
}
insertDrawing(image, originalCursor) {
let newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
@@ -365,9 +372,13 @@ class MarkdownEditor {
// Show draw.io if enabled and handle save.
actionEditDrawing(imgContainer) {
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
let cursorPos = this.cm.getCursor('from');
let drawingId = imgContainer.getAttribute('drawio-diagram');
const drawingDisabled = document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true';
if (drawingDisabled) {
return;
}
const cursorPos = this.cm.getCursor('from');
const drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(() => {
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {

View File

@@ -123,20 +123,21 @@ class PageDisplay {
setupStickySidebar() {
// Make the sidebar stick in view on scroll
let $window = $(window);
let $sidebar = $("#sidebar .scroll-body");
let $bookTreeParent = $sidebar.parent();
const $window = $(window);
const $sidebar = $("#sidebar .scroll-body");
const $sidebarContainer = $sidebar.parent();
const sidebarHeight = $sidebar.height() + 32;
// Check the page is scrollable and the content is taller than the tree
let pageScrollable = ($(document).height() > ($window.height() + 40)) && ($sidebar.height() < $('.page-content').height());
const pageScrollable = ($(document).height() > ($window.height() + 40)) && (sidebarHeight < $('.page-content').height());
// Get current tree's width and header height
let headerHeight = $("#header").height() + $(".toolbar").height();
const headerHeight = $("#header").height() + $(".toolbar").height();
let isFixed = $window.scrollTop() > headerHeight;
// Fix the tree as a sidebar
function stickTree() {
$sidebar.width($bookTreeParent.width() + 15);
$sidebar.width($sidebarContainer.width() + 15);
$sidebar.addClass("fixed");
isFixed = true;
}

View File

@@ -4,22 +4,24 @@ import DrawIO from "../services/drawio";
/**
* Handle pasting images from clipboard.
* @param {ClipboardEvent} event
* @param {WysiwygEditor} wysiwygComponent
* @param editor
*/
function editorPaste(event, editor) {
function editorPaste(event, editor, wysiwygComponent) {
if (!event.clipboardData || !event.clipboardData.items) return;
let items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") === -1) continue;
for (let clipboardItem of event.clipboardData.items) {
if (clipboardItem.type.indexOf("image") === -1) continue;
event.preventDefault();
let id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif');
let file = items[i].getAsFile();
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');
const file = clipboardItem.getAsFile();
setTimeout(() => {
editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
uploadImageFile(file).then(resp => {
uploadImageFile(file, wysiwygComponent).then(resp => {
editor.dom.setAttrib(id, 'src', resp.thumbs.display);
}).catch(err => {
editor.dom.remove(id);
@@ -33,9 +35,12 @@ function editorPaste(event, editor) {
/**
* Upload an image file to the server
* @param {File} file
* @param {WysiwygEditor} wysiwygComponent
*/
function uploadImageFile(file) {
if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
async function uploadImageFile(file, wysiwygComponent) {
if (file === null || file.type.indexOf('image') !== 0) {
throw new Error(`Not an image file`);
}
let ext = 'png';
if (file.name) {
@@ -43,11 +48,13 @@ function uploadImageFile(file) {
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
const remoteFilename = "image-" + Date.now() + "." + ext;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', wysiwygComponent.pageId);
return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
const resp = await window.$http.post(window.baseUrl('/images/gallery/upload'), formData);
return resp.data;
}
function registerEditorShortcuts(editor) {
@@ -370,7 +377,10 @@ class WysiwygEditor {
constructor(elem) {
this.elem = elem;
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
const pageEditor = document.getElementById('page-editor');
this.pageId = pageEditor.getAttribute('page-id');
this.textDirection = pageEditor.getAttribute('text-direction');
this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
this.loadPlugins();
@@ -397,6 +407,9 @@ class WysiwygEditor {
}
getTinyMceConfig() {
const context = this;
return {
selector: '#html-editor',
content_css: [
@@ -586,7 +599,7 @@ class WysiwygEditor {
});
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor));
editor.on('paste', event => editorPaste(event, editor, context));
}
};
}

View File

@@ -8,6 +8,7 @@ import 'codemirror/mode/diff/diff';
import 'codemirror/mode/go/go';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/lua/lua';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/nginx/nginx';
import 'codemirror/mode/php/php';
@@ -38,12 +39,13 @@ const modeMap = {
javascript: 'javascript',
json: {name: 'javascript', json: true},
js: 'javascript',
php: 'php',
lua: 'lua',
md: 'markdown',
mdown: 'markdown',
markdown: 'markdown',
nginx: 'nginx',
powershell: 'powershell',
php: 'php',
py: 'python',
python: 'python',
ruby: 'ruby',

View File

@@ -16,6 +16,7 @@ function mounted() {
addRemoveLinks: true,
dictRemoveFile: trans('components.image_upload_remove'),
timeout: Number(window.uploadTimeout) || 60000,
maxFilesize: Number(window.uploadLimit) || 256,
url: function() {
return _this.uploadUrl;
},

View File

@@ -210,7 +210,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin-left: 1px;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #DDD;
@@ -524,8 +523,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
font-size: 12px;
line-height: 1.2;
top: 88px;
left: -26px;
width: 148px;
left: -12px;
width: 160px;
background: $negative;
padding: $-xs;
color: white;
@@ -535,7 +534,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
content: '';
position: absolute;
top: -6px;
left: 64px;
left: 44px;
width: 0;
height: 0;
border-left: 6px solid transparent;

View File

@@ -51,15 +51,22 @@
margin: $-xs $-s $-xs 0;
}
.align-right {
float: right !important;
text-align: right !important;
}
img.align-right, table.align-right {
text-align: right;
float: right !important;
margin: $-xs 0 $-xs $-s;
}
.align-center {
text-align: center;
}
img.align-center {
display: block;
}
img.align-center, table.align-center {
margin-left: auto;
margin-right: auto;
}
img {
max-width: 100%;
height:auto;

View File

@@ -16,6 +16,14 @@ body {
margin: 0 auto;
}
.flex-fill {
display: block;
}
.flex.sidebar + .flex.content {
border-left: none;
}
.print-hidden {
display: none;
}
@@ -31,4 +39,8 @@ h2 {
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em;
}
.comments-container {
display: none;
}

View File

@@ -60,6 +60,39 @@ return [
'search_created_after' => 'Erstellt nach',
'search_set_date' => 'Datum auswählen',
'search_update' => 'Suche aktualisieren',
/*
* Shelves
*/
'shelf' => 'Regal',
'shelves' => 'Regale',
'shelves_long' => 'Bücherregal',
'shelves_empty' => 'Es wurden noch keine Regale angelegt',
'shelves_create' => 'Erzeuge ein Regal',
'shelves_popular' => 'Beliebte Regale',
'shelves_new' => 'Kürzlich erstellte Regale',
'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',
'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',
'shelves_save' => 'Regal speichern',
'shelves_books' => 'Bücher in diesem Regal',
'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',
'shelves_drag_books' => 'Bücher hier hin ziehen um sie dem Regal hinzuzufügen',
'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',
'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',
'shelves_edit_named' => 'Bücherregal :name bearbeiten',
'shelves_edit' => 'Bücherregal bearbeiten',
'shelves_delete' => 'Bücherregal löschen',
'shelves_delete_named' => 'Bücherregal :name löschen',
'shelves_delete_explain' => "Sie sind im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
'shelves_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Bücherregal löschen wollen?',
'shelves_permissions' => 'Regal-Berechtigungen',
'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
'shelves_copy_permissions' => 'Berechtigungen kopieren',
'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
'shelves_copy_permission_success' => 'Regal-Berechtigungen wurden zu :count Büchern kopiert',
/**
* Books
*/

View File

@@ -9,6 +9,13 @@ return [
'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',
/**
* Shelves
*/
'shelves_delete_explain' => "Du bist im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
'shelves_delete_confirmation' => 'Bist du sicher, dass du dieses Bücherregal löschen willst?',
'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
/**
* Books
*/

View File

@@ -33,6 +33,7 @@ return [
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'image_extension' => 'The :attribute must have a valid & supported image extension.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',

View File

@@ -27,7 +27,7 @@ return [
'email' => 'Email',
'password' => 'Wachtwoord',
'password_confirm' => 'Wachtwoord Bevestigen',
'password_hint' => 'Minimaal 5 tekens',
'password_hint' => 'Minimaal 6 tekens',
'forgot_password' => 'Wachtwoord vergeten?',
'remember_me' => 'Mij onthouden',
'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
@@ -73,4 +73,4 @@ return [
'email_not_confirmed_click_link' => 'Klik op de link in de e-mail die vlak na je registratie is verstuurd.',
'email_not_confirmed_resend' => 'Als je deze e-mail niet kunt vinden kun je deze met onderstaande formulier opnieuw verzenden.',
'email_not_confirmed_resend_button' => 'Bevestigingsmail Opnieuw Verzenden',
];
];

View File

@@ -48,7 +48,7 @@
</form>
</div>
<div class="links text-center">
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
@if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>

View File

@@ -21,7 +21,9 @@
<a @click="updateLanguage('Java')">Java</a>
<a @click="updateLanguage('JavaScript')">JavaScript</a>
<a @click="updateLanguage('JSON')">JSON</a>
<a @click="updateLanguage('Lua')">Lua</a>
<a @click="updateLanguage('PHP')">PHP</a>
<a @click="updateLanguage('Powershell')">Powershell</a>
<a @click="updateLanguage('MarkDown')">MarkDown</a>
<a @click="updateLanguage('Nginx')">Nginx</a>
<a @click="updateLanguage('Python')">Python</a>
@@ -48,4 +50,4 @@
</div>
</div>
</div>
</div>

View File

@@ -34,7 +34,7 @@
@endif
</td>
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td><small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td>{{ $revision->summary }}</td>
<td class="actions">
<a href="{{ $revision->getUrl('changes') }}" target="_blank">{{ trans('entities.pages_revisions_changes') }}</a>

View File

@@ -17,15 +17,17 @@
@if(userCan('page-update', $page))
<a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" >@icon('edit'){{ trans('common.edit') }}</a>
@endif
@if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
@if((userCan('page-view', $page) && userCanOnAny('page-create')) || userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
<ul>
@if(userCan('page-update', $page))
@if(userCanOnAny('page-create'))
<li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
@if(userCan('page-delete', $page))
<li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
@endif
@endif
@if(userCan('page-delete', $page) && userCan('page-update', $page))
<li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
@endif
@if(userCan('page-update', $page))
<li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
@endif
@if(userCan('restrictions-manage', $page))

View File

@@ -38,8 +38,9 @@
<div class="form-group">
<label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
<select name="setting[language]" id="user-language">
@foreach(trans('settings.language_select') as $lang => $label)
<option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
<option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
@endforeach
</select>
</div>

View File

@@ -23,6 +23,7 @@ class LdapTest extends BrowserKitTest
'auth.method' => 'ldap',
'services.ldap.base_dn' => 'dc=ldap,dc=local',
'services.ldap.email_attribute' => 'mail',
'services.ldap.display_name_attribute' => 'cn',
'services.ldap.user_to_groups' => false,
'auth.providers.users.driver' => 'ldap',
]);
@@ -45,6 +46,15 @@ class LdapTest extends BrowserKitTest
});
}
protected function mockUserLogin()
{
return $this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In');
}
public function test_login()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
@@ -60,11 +70,7 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
$this->mockUserLogin()
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
@@ -90,11 +96,7 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
$this->mockEscapes(2);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
$this->mockUserLogin()
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
@@ -115,11 +117,7 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
$this->mockEscapes(2);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
$this->mockUserLogin()
->seePageIs('/login')->see('These credentials do not match our records.')
->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
}
@@ -196,12 +194,7 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(5);
$this->mockExplodes(6);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$this->mockUserLogin()->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -249,12 +242,7 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(4);
$this->mockExplodes(2);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$this->mockUserLogin()->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -303,12 +291,7 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(4);
$this->mockExplodes(2);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$this->mockUserLogin()->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -354,12 +337,7 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(5);
$this->mockExplodes(6);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$this->mockUserLogin()->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -372,4 +350,63 @@ class LdapTest extends BrowserKitTest
]);
}
public function test_login_uses_specified_display_name_attribute()
{
app('config')->set([
'services.ldap.display_name_attribute' => 'displayName'
]);
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')],
'displayName' => 'displayNameAttribute'
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->mockUserLogin()
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
->press('Log In')
->seePageIs('/')
->see('displayNameAttribute')
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
}
public function test_login_uses_default_display_name_attribute_if_specified_not_present()
{
app('config')->set([
'services.ldap.display_name_attribute' => 'displayName'
]);
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->mockUserLogin()
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
->press('Log In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace Tests;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
@@ -27,6 +29,22 @@ class BookShelfTest extends TestCase
$resp->assertElementContains('header', 'Shelves');
}
public function test_shelves_shows_in_header_if_have_any_shelve_view_permission()
{
$user = factory(User::class)->create();
$this->giveUserPermissions($user, ['image-create-all']);
$shelf = Bookshelf::first();
$userRole = $user->roles()->first();
$resp = $this->actingAs($user)->get('/');
$resp->assertElementNotContains('header', 'Shelves');
$this->setEntityRestrictions($shelf, ['view'], [$userRole]);
$resp = $this->get('/');
$resp->assertElementContains('header', 'Shelves');
}
public function test_shelves_page_contains_create_link()
{
$resp = $this->asEditor()->get('/shelves');

View File

@@ -1,5 +1,6 @@
<?php namespace Tests;
use BookStack\Auth\Role;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
@@ -239,4 +240,35 @@ class SortTest extends TestCase
$this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
}
public function test_page_can_be_copied_without_edit_permission()
{
$page = Page::first();
$currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first();
$viewer = $this->getViewer();
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertDontSee($page->getUrl('/copy'));
$newBook->created_by = $viewer->id;
$newBook->save();
$this->giveUserPermissions($viewer, ['page-create-own']);
$this->regenEntityPermissions($newBook);
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertSee($page->getUrl('/copy'));
$movePageResp = $this->post($page->getUrl('/copy'), [
'entity_selection' => 'book:' . $newBook->id,
'name' => 'My copied test page'
]);
$movePageResp->assertRedirect();
$this->assertDatabaseHas('pages', [
'name' => 'My copied test page',
'created_by' => $viewer->id,
'book_id' => $newBook->id,
]);
}
}

View File

@@ -39,13 +39,57 @@ class ImageTest extends TestCase
]);
}
public function test_php_files_cannot_be_uploaded()
{
$page = Page::first();
$admin = $this->getAdmin();
$this->actingAs($admin);
$fileName = 'bad.php';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$file = $this->getTestImage($fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
$this->assertDatabaseMissing('images', [
'type' => 'gallery',
'name' => $fileName
]);
}
public function test_php_like_files_cannot_be_uploaded()
{
$page = Page::first();
$admin = $this->getAdmin();
$this->actingAs($admin);
$fileName = 'bad.phtml';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$file = $this->getTestImage($fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
$this->assertDatabaseMissing('images', [
'type' => 'gallery',
'name' => $fileName
]);
}
public function test_secure_images_uploads_to_correct_place()
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload');
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::first();
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);
@@ -61,9 +105,9 @@ class ImageTest extends TestCase
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload');
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::first();
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$imageUrl = json_decode($upload->getContent(), true)['url'];
@@ -84,9 +128,9 @@ class ImageTest extends TestCase
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-system-test-upload');
$galleryFile = $this->getTestImage('my-system-test-upload.png');
$page = Page::first();
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload');
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png');
$upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);

View File

@@ -1,6 +1,8 @@
<?php namespace Tests\Uploads;
use Illuminate\Http\UploadedFile;
trait UsesImages
{
/**
@@ -15,11 +17,11 @@ trait UsesImages
/**
* Get a test image that can be uploaded
* @param $fileName
* @return \Illuminate\Http\UploadedFile
* @return UploadedFile
*/
protected function getTestImage($fileName)
{
return new \Illuminate\Http\UploadedFile($this->getTestImageFilePath(), $fileName, 'image/png', 5238);
return new UploadedFile($this->getTestImageFilePath(), $fileName, 'image/png', 5238, null, true);
}
/**
@@ -46,12 +48,14 @@ trait UsesImages
* Uploads an image with the given name.
* @param $name
* @param int $uploadedTo
* @param string $contentType
* @return \Illuminate\Foundation\Testing\TestResponse
*/
protected function uploadImage($name, $uploadedTo = 0)
protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png')
{
$file = $this->getTestImage($name);
return $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
return $this->withHeader('Content-Type', $contentType)
->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**

BIN
tests/test-data/bad.php Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

BIN
tests/test-data/bad.phtml Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

View File

@@ -1 +1 @@
v0.25.0
v0.25.4