Compare commits

...

146 Commits

Author SHA1 Message Date
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
e3eefba745 Fixed export testing and updated travis settings 2017-02-26 21:39:15 +00:00
Dan Brown
a90f564980 Made LDAP email attribute configurable via .env
Closes #306
2017-02-26 14:51:49 +00:00
Dan Brown
253132afdf Added chapter export options
Closes #177
2017-02-26 14:25:02 +00:00
Dan Brown
eded8abded Added book export and created export tests to cover
In reference to #177
2017-02-26 13:26:51 +00:00
Dan Brown
0abed1afe5 Added clear activity/revision commands. Cleaned commands.
Added testing to cover each command.
Removed example laravel inspire command.
Standardised command names to be behind 'bookstack' naming.
In reference to #320.
2017-02-26 09:16:24 +00:00
Dan Brown
22077d4181 Updated DOMPDF to latest version 2017-02-25 14:59:56 +00:00
Dan Brown
b0e849f413 Added checkbox sytax parsing to markdown lists
Closes #319
2017-02-25 13:16:26 +00:00
Dan Brown
af3c0e43a5 Prevented custom HTML being inserted on settings page
Gives option for fixing if badly formatted HTML is inserted.
Closes #310
2017-02-25 12:41:32 +00:00
Dan Brown
387047f262 Fixed inaccessible revisions, added regression tests
Fixes #309
2017-02-25 12:29:01 +00:00
Dan Brown
4a2a539c08 Merge pull request #295 from ReeseSebastian/master
Updated and improved german translation
2017-02-23 19:19:21 +00:00
Dan Brown
a6128a1df1 Merge bugfixes from branch 'v0.14' 2017-02-05 21:24:15 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
6638ee47d3 Fixed entities wrongly visible on 404
Also ensured header state as expected on 404.
In reference to BookStackApp/website#9
2017-02-05 21:19:29 +00:00
Dan Brown
65899a3e91 Prevented settings being overfetched from db/cache 2017-02-05 18:57:57 +00:00
Dan Brown
86625a7642 Neatened up social login/register buttons 2017-02-05 15:28:53 +00:00
Dan Brown
ee495450cc Improved multi-line callout rendering
Closes #300
2017-02-05 14:47:26 +00:00
Dan Brown
d369d315a7 Fixed non-browserkit testcase and seeder issues 2017-02-05 14:37:50 +00:00
Dan Brown
7c9937e924 Converted sort tests to non browserkit testing
Added testing to cover book sort endpoint.
Closes #283
2017-02-05 14:20:59 +00:00
Dan Brown
33a2999a57 Namespaced tests to align with new laravel default 2017-02-04 11:58:42 +00:00
Dan Brown
076693efc9 Added facebook, slack & twitter sign in options.
Also added icon svg blade helper.
Closes #125. Starts #213.
Requires documentation.
2017-02-04 11:01:49 +00:00
Sebastian Reese
54d1fcde5b Merge branch 'master' of https://github.com/BookStackApp/BookStack 2017-02-02 00:43:54 +01:00
Sebastian Reese
1c656d6556 Updated and improved german translation 2017-02-02 00:43:24 +01:00
Dan Brown
2431ce9f86 Merge branch 'v0.14' 2017-02-01 22:28:38 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
80f844139c Fixed missing subscript styling
Closes #284
2017-02-01 22:20:44 +00:00
Dan Brown
9eecaea31a Attempt to fix bookchildren and user getThumb
Hopefully Fixes #292 and #294 and #287
2017-02-01 22:16:32 +00:00
Dan Brown
3ccfa0e7fc Fixed readme badge links & added contributing block 2017-01-30 19:31:24 +00:00
Dan Brown
6669998c10 Upgraded to Laravel 5.4 2017-01-25 19:35:40 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
492e2f173e Fixed error causing permissions to be deleted on book sort
Closes #282
2017-01-23 22:27:11 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
380f0f2042 Prevented a missing avatar from crashing the application 2017-01-22 12:19:50 +00:00
Dan Brown
33d4844f17 Fixed role 'manage own permissions' permission 2017-01-22 12:16:02 +00:00
Dan Brown
989de47f22 Fixed hidden book children for admins on upgrade 2017-01-22 12:02:30 +00:00
Dan Brown
8f19231ed5 Added options to use database cache & sessions 2017-01-21 16:39:50 +00:00
Dan Brown
5c60f27a7d Merge branch 'page_transclusion'
Closes #123
2017-01-21 16:17:33 +00:00
Dan Brown
2d4034f3b7 Added transclusion tests and fixed other tests 2017-01-21 16:16:27 +00:00
Dan Brown
56d58ad8e5 Updated pointer to be able to show includes
Also fixed pointer copying on flash-blocked browsers
2017-01-21 14:58:03 +00:00
Dan Brown
a4f6bc63f0 Updated page include logic to use blade-style tags
It will also snippets of a page if and id is provided in a tag
2017-01-21 13:53:00 +00:00
Dan Brown
26da81a3b0 Added pt_BR to language dropdown and renamed folder 2017-01-18 20:04:29 +00:00
Dan Brown
ec9410b510 Merge pull request #279 from NakaharaL/master
Brazilian Portuguese Localization
2017-01-18 19:48:53 +00:00
NakaharaL
a0035e4de2 Brazilian Portuguese Localization
Translated files for messages from English to Brazilian Portuguese
2017-01-18 08:29:18 -03:00
Dan Brown
e4e3b25c22 Started page transclusion system 2017-01-16 21:24:48 +00:00
Dan Brown
d8c5f72258 Updated issue template and added TinyMCE autolinking
Closes #267
2017-01-16 19:33:29 +00:00
Dan Brown
d67ad47b2c Merge pull request #274 from GeneralMediaCH/feature/translations
French translations
2017-01-16 19:23:30 +00:00
Dan Brown
b08e49b59d Updated CI to not use github token 2017-01-16 19:14:58 +00:00
sirgix
d3dc73ca06 remove trailing spaces 2017-01-16 15:34:34 +01:00
sirgix
d5ea15e6dd fix typo 2017-01-16 14:51:45 +01:00
sirgix
5d45286646 french translation 2017-01-16 14:51:33 +01:00
Dan Brown
dabf149411 Added user setting system and added user-lang option
Supports #115
2017-01-15 16:27:24 +00:00
Dan Brown
ee5ded6e1e Updated permission bookChildrenQuery to use QueryBuilder 2017-01-15 15:00:29 +00:00
Dan Brown
598b07b53d Updated LDAP to allow protocol to be specified
As per details by fredericmohr in #236
2017-01-14 17:55:09 +00:00
Dan Brown
e211f31370 Merge branch 'patch-2' of git://github.com/fredericmohr/BookStack into fredericmohr-patch-2 2017-01-14 17:30:49 +00:00
Dan Brown
0bcf608e0b Fixed page navigation with special chars in id
Also made so it smooth-scrolls and uses app theme color.
Fixes #254
2017-01-14 16:36:12 +00:00
Dan Brown
969ad8911c Updated page nav to hide when empty 2017-01-14 15:34:52 +00:00
Dan Brown
581c382f65 Fixed image delete permission issue
Also fixed missing translations and wrote tests to cover issue.
Fixes #258
2017-01-08 19:19:30 +00:00
Dan Brown
f7f86ff821 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-01-08 18:43:33 +00:00
Dan Brown
212cd710aa Fixed default empty app settings effecting blank app color
Fixes #265
2017-01-08 18:42:46 +00:00
Dan Brown
33c44d3c0f Merge pull request #263 from Abijeet/typo-fix
Typo fix
2017-01-08 13:55:44 +00:00
Dan Brown
0faa130cfd Fixed offset code blocks when editing in markdown.
Fixes #264
2017-01-08 13:31:53 +00:00
Abijeet Patro
af76580b98 Fixes typo causing the message not to be displayed 2017-01-03 22:18:13 +05:30
Abijeet Patro
b526d172d6 Merge pull request #5 from BookStackApp/master
Getting the latest from Bookstack to push the typo-fix
2017-01-03 22:16:39 +05:30
Dan Brown
f2917fc462 Added tests to cover social login actions
Closes #244
2017-01-02 14:56:58 +00:00
Dan Brown
7c8c4c2a05 Normalised page nav header inset when only small headers are used 2017-01-02 12:13:03 +00:00
Dan Brown
3595ac2551 Merge pull request #262 from BookStackApp/entity_repo_refactor
Entity repo refactor
2017-01-02 11:12:44 +00:00
Dan Brown
8453191dfb Finished refactor of entity repos
Removed entity-specific repos and standardised
the majority of repo calls to be applicable to
all entity types
2017-01-02 11:07:27 +00:00
Dan Brown
65796cfc7b Rewrote book children query 2017-01-01 21:21:11 +00:00
Dan Brown
bab27462ab Fixed issue where default user was over-fetched 2017-01-01 17:33:06 +00:00
Dan Brown
241278226f Refactored search and slug repo components 2017-01-01 16:57:47 +00:00
Dan Brown
7f9de2c8ab Started refactor to merge entity repos 2017-01-01 16:05:44 +00:00
Dan Brown
f91f33c236 Updated readme attribution and npm scripts 2017-01-01 12:51:23 +00:00
Dan Brown
3f0ef57d31 Added wkhtmltopdf support and done some style tweaks
Closes #248
2017-01-01 12:20:30 +00:00
Dan Brown
0eb90cb3b6 Fixed carbon locale setting 2016-12-31 14:38:04 +00:00
Dan Brown
9fe158b78a Merge pull request #255 from olexus/master
Add Carbon localization support
2016-12-31 14:36:01 +00:00
Dan Brown
b14222dabd Merge pull request #234 from BookStackApp/translations
Setup for translations
2016-12-31 14:33:25 +00:00
Dan Brown
a24f3d7d47 Merge branch 'master' into translations 2016-12-31 14:32:52 +00:00
Dan Brown
c9700e38e2 Created solution for JS translations
Also tidied up existing components and JS
2016-12-31 14:27:40 +00:00
Dan Brown
05316c90ba converted image picker to blade-based component
Also updated some other JS translations
2016-12-24 15:21:19 +00:00
Dan Brown
242dc21876 Converted toggle switch into a blade/jquery template 2016-12-22 19:41:32 +00:00
Dan Brown
08c4b9ac7c Standardised JS vars and imports/exports 2016-12-19 19:16:31 +00:00
olexus
f30f4579e9 Add Carbon localization support 2016-12-19 11:05:06 +03:00
Dan Brown
573357a08c Extracted text from logic files 2016-12-04 16:51:39 +00:00
Dan Brown
0775cd09a1 Extracted text for remaining views 2016-12-04 14:08:04 +00:00
Dan Brown
96075dee7b Extracted text from page views & standardised breadcrumbs 2016-12-03 18:35:40 +00:00
Dan Brown
066adf3cea Moved text for errors and form views.
Updated 404 page with additional links
2016-12-03 13:31:54 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
286f9b0c7d Moved page tags to sidebar
Fixed #238
2016-11-27 19:37:57 +00:00
Dan Brown
c403d05755 Fixed social login routes
Fixes #239
2016-11-27 19:11:15 +00:00
Dan Brown
57dc53ceff Extracted text from book & chapter views 2016-11-17 13:33:07 +00:00
Frederic Mohr
340d3f833b Update Ldap.php
This is a very crude workaround, a better solution is explained in the comments I added.
2016-11-15 11:10:12 +01:00
Dan Brown
694a9459c1 Moved text from auth views into lang files 2016-11-13 16:34:28 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
0fb1fc87c8 Enabled utf8 slugs
Prevents slug change when using only non-ascii chars
Allows use of more localised urls.

Closes #233
2016-11-12 17:16:52 +00:00
Dan Brown
d3c7aada89 Fixed attachments on draft pages 2016-11-12 14:21:54 +00:00
Dan Brown
e639600ba5 Renamed files to attachments 2016-11-12 14:12:26 +00:00
Dan Brown
d439fb459b Made language configurable via .env file 2016-11-12 13:30:00 +00:00
Dan Brown
9d8a176182 Merge branch 'localization-de' of git://github.com/robertlandes/BookStack into robertlandes-localization-de 2016-11-12 13:27:37 +00:00
Dan Brown
600055bc73 Fixed tag searches and added tag search regression test
Fixes #222
2016-11-12 13:21:16 +00:00
Robert Landes
e691b401c8 Added initial translation into German (formal)
This translation is based on the current english language files. It is a
transaltion into German (formal) language.  A language selector on the
Settings page would be a nice addition.
2016-11-12 14:16:07 +01:00
Dan Brown
672b15d36c Fixed attachment base-url usage and non-existant images
Images now self-delete if the original file does not exist.
Prevents simply getting non-fixable errors.

Also cleaned some JS.
2016-11-12 12:41:34 +00:00
Dan Brown
ac80723058 Merge fixes from branch 'v0.12' 2016-11-12 11:40:54 +00:00
Dan Brown
ab468bac3c Updated build and versioning system
Added versioning file instead of using git tags
(Step towards removing git as a dependancy in the future)

Updated gulpfile to fit with verisioning system and cleaned
up node dependancies.

Fixes #108
2016-10-30 17:44:00 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
2af0021c2b Fixed image tests after amends to url system 2016-10-30 12:58:01 +00:00
Dan Brown
0f2eaccb39 Added quick test to cover hypen breakage 2016-10-30 12:15:11 +00:00
Dan Brown
b251671e3f Amended search to not break on non-alpha-num chars
And also fixed exact term matches that contain non-alpha-num chars
Fixes #212
2016-10-30 12:08:22 +00:00
Dan Brown
c4eed37d8e Added custom head content into public pages
Closes #211
2016-10-30 11:46:23 +00:00
Dan Brown
8b43b91057 Improved password reset flow with notifications.
Also added links to sign-in/register.
Fixed links in emails sent out.

Fixes #210 and #218.
2016-10-30 11:36:57 +00:00
Dan Brown
91fe7f0bee Fixed PDF export table width
Closes #203

Signed-off-by: Dan Brown <ssddanbrown@googlemail.com>
2016-10-30 10:41:18 +00:00
Dan Brown
5cfb7b8de4 Altered 'ol' element padding to not clip numbering
Allows usage to 3-digits now
Closes #204
2016-10-30 10:23:49 +00:00
Dan Brown
6329a1842a Fixed issue with callouts overflowing page tags
Closes #179
2016-10-30 10:19:45 +00:00
Dan Brown
a6c6c6e300 Merge pull request #205 from ssddanbrown/attachments
Implementation of File Attachments
2016-10-23 18:01:20 +01:00
Dan Brown
30458405ce Page Attachments - Improved UI, Now initially complete
Closes #62
2016-10-23 17:55:48 +01:00
Dan Brown
91220239e5 Added in attachment tests 2016-10-23 15:25:04 +01:00
Dan Brown
7ee695d74a File upload deletion complete & added extension handling
Also fixed issue with file editing on JS side
2016-10-23 13:36:45 +01:00
Dan Brown
867fc8be64 Added basic attachment editing functionality 2016-10-11 20:39:11 +01:00
Dan Brown
89509b487a Added attachment creation from link/name 2016-10-10 21:13:18 +01:00
Dan Brown
ac0b29fb6d Added view, deletion and permissions for files 2016-10-10 20:30:27 +01:00
Dan Brown
673c74ddfc Started work on attachments
Created base models and started user-facing controls.
2016-10-09 18:58:22 +01:00
Dan Brown
3b7d223b0c Updated and added tests for new default user system
Closes #138
2016-09-29 17:07:58 +01:00
Dan Brown
b662670efc Prevented guest users creating draft pages. 2016-09-29 15:56:57 +01:00
Dan Brown
771626b6ec Started work on making the public role/user configurable
Create a new 'public' guest user and made the public
role visible on role setting screens.
2016-09-29 12:43:46 +01:00
Dan Brown
f15cc5bdfa Separated revision preview and diff & fixed chosen diff html
Closes #8
2016-09-29 10:10:46 +01:00
Dan Brown
fff5bbcee4 Merge branch 'diff' of git://github.com/younes0/BookStack into younes0-diff 2016-09-29 09:32:40 +01:00
Dan Brown
42d8e9e5bd Improved numeric term search capabilities
Prevented a quoted term also being added to fuzzy searches
and also added check to see if the term is numeric to check if
an exact match is required.

Closes #200
2016-09-29 09:13:15 +01:00
Dan Brown
da10c50945 Added app name header display setting + extracted setting text
Closes #194
2016-09-22 18:53:22 +01:00
Dan Brown
24523cf31d Added page autosave request failure notification
Closes #192
2016-09-18 15:10:27 +01:00
Dan Brown
1d681e53e4 Added page navigation and tweaked header styles
Changed header selection in editor to be more descriptive and
to provide a wider range of styles.

Closes #68
2016-09-18 14:49:36 +01:00
Dan Brown
e0235fda8b Made registration gravatar/email requests fail gracefully
* Extracted any email confirmation text into langs.
* Added new notification on confirmation email send fail.

Closes #187
2016-09-17 21:33:55 +01:00
Dan Brown
9dc9724e15 Laravel 5.3 upgrade (#189)
* Started move to laravel 5.3

* Started updating login & registration flows for laravel 5.3 update

* Updated app emails to notification system

* Fixed registations bugs and removed email confirmation model

* Fixed large portion of laravel post-upgrade issues

* Fixed and tested LDAP process
2016-09-17 18:22:04 +01:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
393f6047f2 Updated table styling to ideally by more predictable
Set styles to be fixed and not cut text.
2016-09-06 20:47:34 +01:00
Dan Brown
d94fc5b694 Fixed table cell p tag spacing and de-duped editor shortcuts
Also changed shortcuts to be better compatible with OSX by using the
command key.
2016-09-06 19:50:47 +01:00
Dan Brown
f06770d91d Updated TinyMCE version from 4.3.7 to 4.4.3 2016-09-06 19:29:09 +01:00
Younès EL BIACHE
c279c6e2af replace GPL diff lib with MIT lib
replace gpl lib with mit lib
2016-07-10 12:01:05 +02:00
Younès EL BIACHE
9537e2ae95 html diff in revision view 2016-07-07 19:54:40 +02:00
299 changed files with 11771 additions and 5349 deletions

View File

@@ -1,11 +1,13 @@
### For Feature Requests
Desired Feature:
### For Bug Reports
PHP Version:
MySQL Version:
* BookStack Version:
* PHP Version:
* MySQL Version:
Expected Behavior:
##### Expected Behavior
Actual Behavior:
##### Actual Behavior

5
.gitignore vendored
View File

@@ -10,4 +10,7 @@ Homestead.yaml
/public/bower
/storage/images
_ide_helper.php
/storage/debugbar
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin

View File

@@ -1,5 +1,5 @@
dist: trusty
sudo: required
sudo: false
language: php
php:
- 7.0
@@ -8,18 +8,12 @@ cache:
directories:
- $HOME/.composer/cache
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- mysql -u root -e "FLUSH PRIVILEGES;"
- phpenv config-rm xdebug.ini
- composer self-update
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- php artisan clear-compiled -n
@@ -27,5 +21,8 @@ before_script:
- php artisan migrate --force -n --database=mysql_testing
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
after_failure:
- cat storage/logs/laravel.log
script:
- phpunit

36
app/Attachment.php Normal file
View File

@@ -0,0 +1,36 @@
<?php namespace BookStack;
class Attachment extends Ownable
{
protected $fillable = ['name', 'order'];
/**
* Get the downloadable file name for this upload.
* @return mixed|string
*/
public function getFileName()
{
if (str_contains($this->name, '.')) return $this->name;
return $this->name . '.' . $this->extension;
}
/**
* Get the page this file was uploaded to.
* @return Page
*/
public function page()
{
return $this->belongsTo(Page::class, 'uploaded_to');
}
/**
* Get the url of this file.
* @return string
*/
public function getUrl()
{
return baseUrl('/attachments/' . $this->id);
}
}

View File

@@ -13,9 +13,9 @@ class Book extends Entity
public function getUrl($path = false)
{
if ($path !== false) {
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $this->slug);
return baseUrl('/books/' . urlencode($this->slug));
}
/*

View File

@@ -5,6 +5,8 @@ class Chapter extends Entity
{
protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $with = ['book'];
/**
* Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@@ -16,11 +18,12 @@ class Chapter extends Entity
/**
* Get the pages that this chapter contains.
* @param string $dir
* @return mixed
*/
public function pages()
public function pages($dir = 'ASC')
{
return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
return $this->hasMany(Page::class)->orderBy('priority', $dir);
}
/**
@@ -32,9 +35,9 @@ class Chapter extends Entity
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
if ($path !== false) {
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
}
/**

View File

@@ -0,0 +1,47 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Activity;
use Illuminate\Console\Command;
class ClearActivity extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-activity';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear user activity from the system';
protected $activity;
/**
* Create a new command instance.
*
* @param Activity $activity
*/
public function __construct(Activity $activity)
{
$this->activity = $activity;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->activity->newQuery()->truncate();
$this->comment('System activity cleared');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-revisions
{--a|all : Include active update drafts in deletion}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear page revisions';
protected $pageRevision;
/**
* Create a new command instance.
*
* @param PageRevision $pageRevision
*/
public function __construct(PageRevision $pageRevision)
{
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
$this->comment('Revisions deleted');
}
}

View File

@@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
class ResetViews extends Command
class ClearViews extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'views:reset';
protected $signature = 'bookstack:clear-views';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset all view-counts for all entities.';
protected $description = 'Clear all view-counts for all entities.';
/**
* Create a new command instance.
@@ -37,5 +37,6 @@ class ResetViews extends Command
public function handle()
{
\Views::resetAll();
$this->comment('Views cleared');
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
*
* @var string
*/
protected $signature = 'permissions:regen';
protected $signature = 'bookstack:regenerate-permissions';
/**
* The console command description.
@@ -47,5 +47,6 @@ class RegeneratePermissions extends Command
public function handle()
{
$this->permissionService->buildJointPermissions();
$this->comment('Permissions regenerated');
}
}

View File

@@ -13,8 +13,9 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
\BookStack\Console\Commands\ClearViews::class,
\BookStack\Console\Commands\ClearActivity::class,
\BookStack\Console\Commands\ClearRevisions::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
];
@@ -26,7 +27,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
//
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace BookStack;
class EmailConfirmation extends Model
{
protected $fillable = ['user_id', 'token'];
/**
* Get the user that this confirmation is attached to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -4,6 +4,8 @@
class Entity extends Ownable
{
protected $fieldsToSearch = ['name', 'description'];
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
@@ -157,47 +159,53 @@ class Entity extends Ownable
* @param string[] array $wheres
* @return mixed
*/
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
public function fullTextSearchQuery($terms, $wheres = [])
{
$exactTerms = [];
if (count($terms) === 0) {
$search = $this;
$orderBy = 'updated_at';
} else {
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
$term = '"' . $term . '"';
} else {
$term = '' . $term . '*';
}
if ($term !== '*') $terms[$key] = $term;
}
$termString = implode(' ', $terms);
$fields = implode(',', $fieldsToSearch);
$search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
$fuzzyTerms = [];
$search = static::newQuery();
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
foreach ($exactTerms as $exactTerm) {
foreach ($fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
} else {
$term = '' . $term . '*';
if ($term !== '*') $fuzzyTerms[] = $term;
}
$orderBy = 'title_relevance';
};
}
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
// Perform fulltext search if relevant terms exist.
if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
}
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) {
foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');

View File

@@ -1,8 +0,0 @@
<?php
namespace BookStack\Events;
abstract class Event
{
//
}

View File

@@ -0,0 +1,4 @@
<?php namespace BookStack\Exceptions;
class FileUploadException extends PrettyException {}

View File

@@ -3,9 +3,9 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
@@ -87,4 +87,20 @@ class Handler extends ExceptionHandler
} while ($e = $e->getPrevious());
return $message;
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
}

View File

@@ -0,0 +1,215 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Repos\EntityRepo;
use BookStack\Services\AttachmentService;
use Illuminate\Http\Request;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $attachment;
protected $entityRepo;
/**
* AttachmentController constructor.
* @param AttachmentService $attachmentService
* @param Attachment $attachment
* @param EntityRepo $entityRepo
*/
public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
{
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Endpoint at which attachments are uploaded to.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function upload(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$uploadedFile = $request->file('file');
try {
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($attachment);
}
/**
* Update an uploaded attachment.
* @param int $attachmentId
* @param Request $request
* @return mixed
*/
public function uploadUpdate($attachmentId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$uploadedFile = $request->file('file');
try {
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($attachment);
}
/**
* Update the details of an existing file.
* @param $attachmentId
* @param Request $request
* @return Attachment|mixed
*/
public function update($attachmentId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'url|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return response()->json($attachment);
}
/**
* Attach a link to a page.
* @param Request $request
* @return mixed
*/
public function attachLink(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|url|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$attachmentName = $request->get('name');
$link = $request->get('link');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
return response()->json($attachment);
}
/**
* Get the attachments for a specific page.
* @param $pageId
* @return mixed
*/
public function listForPage($pageId)
{
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
}
/**
* Update the attachment sorting.
* @param $pageId
* @param Request $request
* @return mixed
*/
public function sortForPage($pageId, Request $request)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
]);
$page = $this->entityRepo->getById('page', $pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
return response()->json(['message' => trans('entities.attachments_order_updated')]);
}
/**
* Get an attachment from storage.
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
*/
public function get($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
$this->checkOwnablePermission('page-view', $page);
if ($attachment->external) {
return redirect($attachment->path);
}
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return response($attachmentContents, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
]);
}
/**
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
*/
public function delete($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
return response()->json(['message' => trans('entities.attachments_deleted')]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Password;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function sendResetLinkEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
session()->flash('success', $message);
return back()->with('status', trans($response));
}
// If an error was returned by the password broker, we will get this message
// translated so we can notify a user of the problem. We'll redirect back
// to where the users came from so they can attempt this process again.
return back()->withErrors(
['email' => trans($response)]
);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
parent::__construct();
}
public function username()
{
return config('auth.method') === 'standard' ? 'email' : 'username';
}
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
*/
protected function authenticated(Request $request, Authenticatable $user)
{
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}

View File

@@ -1,62 +1,69 @@
<?php namespace BookStack\Http\Controllers\Auth;
<?php
use BookStack\Exceptions\AuthException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount;
use BookStack\User;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Foundation\Auth\RegistersUsers;
class AuthController extends Controller
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $username = 'email';
use RegistersUsers;
protected $socialAuthService;
protected $emailConfirmationService;
protected $userRepo;
/**
* Create a new authentication controller instance.
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
$this->redirectTo = baseUrl('/');
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
parent::__construct();
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
@@ -69,16 +76,20 @@ class AuthController extends Controller
]);
}
/**
* Check whether or not registrations are allowed in the app settings.
* @throws UserRegistrationException
*/
protected function checkRegistrationAllowed()
{
if (!setting('registration-enabled')) {
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
}
}
/**
* Show the application registration form.
* @return \Illuminate\Http\Response
* @return Response
*/
public function getRegister()
{
@@ -89,9 +100,10 @@ class AuthController extends Controller
/**
* Handle a registration request for the application.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @throws UserRegistrationException
* @throws \Illuminate\Foundation\Validation\ValidationException
*/
public function postRegister(Request $request)
{
@@ -108,66 +120,18 @@ class AuthController extends Controller
return $this->registerUser($userData);
}
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
* Create a new user instance after a valid registration.
* @param array $data
* @return User
*/
protected function authenticated(Request $request, Authenticatable $user)
protected function create(array $data)
{
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
/**
@@ -176,7 +140,7 @@ class AuthController extends Controller
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\ConfirmationEmailException
* @throws ConfirmationEmailException
*/
protected function registerUser(array $userData, $socialAccount = false)
{
@@ -184,7 +148,7 @@ class AuthController extends Controller
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
}
@@ -195,12 +159,18 @@ class AuthController extends Controller
if (setting('registration-confirmation') || setting('registration-restrict')) {
$newUser->save();
$this->emailConfirmationService->sendConfirmation($newUser);
try {
$this->emailConfirmationService->sendConfirmation($newUser);
} catch (Exception $e) {
session()->flash('error', trans('auth.email_confirm_send_error'));
}
return redirect('/register/confirm');
}
auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
session()->flash('success', trans('auth.register_success'));
return redirect($this->redirectPath());
}
@@ -213,18 +183,6 @@ class AuthController extends Controller
return view('auth/register-confirm');
}
/**
* View the confirmation email as a standard web page.
* @param $token
* @return \Illuminate\View\View
* @throws UserRegistrationException
*/
public function viewConfirmEmail($token)
{
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
return view('emails/email-confirmation', ['token' => $confirmation->token]);
}
/**
* Confirms an email via a token and logs the user into the system.
* @param $token
@@ -237,8 +195,8 @@ class AuthController extends Controller
$user = $confirmation->user;
$user->email_confirmed = true;
$user->save();
auth()->login($confirmation->user);
session()->flash('success', 'Your email has been confirmed!');
auth()->login($user);
session()->flash('success', trans('auth.email_confirm_success'));
$this->emailConfirmationService->deleteConfirmationsByUser($user);
return redirect($this->redirectPath);
}
@@ -264,33 +222,19 @@ class AuthController extends Controller
'email' => 'required|email|exists:users,email'
]);
$user = $this->userRepo->getByEmail($request->get('email'));
try {
$this->emailConfirmationService->sendConfirmation($user);
} catch (Exception $e) {
session()->flash('error', trans('auth.email_confirm_send_error'));
return redirect('/register/confirm');
}
$this->emailConfirmationService->sendConfirmation($user);
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
session()->flash('success', trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
/**
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
@@ -319,7 +263,7 @@ class AuthController extends Controller
return $this->socialRegisterCallback($socialDriver);
}
} else {
throw new SocialSignInException('No action defined', '/login');
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
return redirect()->back();
}
@@ -334,4 +278,24 @@ class AuthController extends Controller
return $this->socialAuthService->detachSocialAccount($socialDriver);
}
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
}
}

View File

@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
@@ -23,10 +23,27 @@ class PasswordController extends Controller
protected $redirectTo = '/';
/**
* Create a new password controller instance.
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
}
/**
* Get the response for a successful password reset.
*
* @param string $response
* @return \Illuminate\Http\Response
*/
protected function sendResetResponse($response)
{
$message = trans('auth.reset_password_success');
session()->flash('success', $message);
return redirect($this->redirectPath())
->with('status', trans($response));
}
}

View File

@@ -1,35 +1,31 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Illuminate\Http\Response;
use Views;
class BookController extends Controller
{
protected $bookRepo;
protected $pageRepo;
protected $chapterRepo;
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@@ -39,9 +35,9 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->bookRepo->getAllPaginated(10);
$recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false;
$popular = $this->bookRepo->getPopular(4, 0);
$books = $this->entityRepo->getAllPaginated('book', 10);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$this->setPageTitle('Books');
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
}
@@ -53,7 +49,7 @@ class BookController extends Controller
public function create()
{
$this->checkPermission('book-create-all');
$this->setPageTitle('Create New Book');
$this->setPageTitle(trans('entities.books_create'));
return view('books/create');
}
@@ -70,7 +66,7 @@ class BookController extends Controller
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->bookRepo->createFromInput($request->all());
$book = $this->entityRepo->createFromInput('book', $request->all());
Activity::add($book, 'book_create', $book->id);
return redirect($book->getUrl());
}
@@ -82,9 +78,9 @@ class BookController extends Controller
*/
public function show($slug)
{
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->bookRepo->getChildren($book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
$this->setPageTitle($book->getShortName());
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
@@ -97,9 +93,9 @@ class BookController extends Controller
*/
public function edit($slug)
{
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle('Edit Book ' . $book->getShortName());
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
}
@@ -111,13 +107,13 @@ class BookController extends Controller
*/
public function update(Request $request, $slug)
{
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->bookRepo->updateFromInput($book, $request->all());
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -129,9 +125,9 @@ class BookController extends Controller
*/
public function showDelete($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle('Delete Book ' . $book->getShortName());
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]);
}
@@ -142,11 +138,11 @@ class BookController extends Controller
*/
public function sort($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book, true);
$books = $this->bookRepo->getAll(false);
$this->setPageTitle('Sort Book ' . $book->getShortName());
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
@@ -158,8 +154,8 @@ class BookController extends Controller
*/
public function getSortItem($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = $this->bookRepo->getChildren($book);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
@@ -171,7 +167,7 @@ class BookController extends Controller
*/
public function saveSort($bookSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
@@ -190,13 +186,13 @@ class BookController extends Controller
$priority = $bookChild->sort;
$id = intval($bookChild->id);
$isPage = $bookChild->type == 'page';
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
// Update models only if there's a change in parent chain or ordering.
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
$model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId;
$model->save();
@@ -211,12 +207,12 @@ class BookController extends Controller
// Add activity for books
foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId);
$updatedBook = $this->entityRepo->getById('book', $bookId);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
$this->bookRepo->buildJointPermissions($updatedModels);
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}
@@ -228,11 +224,10 @@ class BookController extends Controller
*/
public function destroy($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book);
$this->bookRepo->destroy($book);
$this->entityRepo->destroyBook($book);
return redirect('/books');
}
@@ -243,7 +238,7 @@ class BookController extends Controller
*/
public function showRestrict($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
@@ -261,10 +256,55 @@ class BookController extends Controller
*/
public function restrict($bookSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
session()->flash('success', 'Book Restrictions Updated');
$this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
}
/**
* Export a book as a PDF file.
* @param string $bookSlug
* @return mixed
*/
public function exportPdf($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$pdfContent = $this->exportService->bookToPdf($book);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
]);
}
/**
* Export a book as a contained HTML file.
* @param string $bookSlug
* @return mixed
*/
public function exportHtml($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
]);
}
/**
* Export a book as a plain text file.
* @param $bookSlug
* @return mixed
*/
public function exportPlainText($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToPlainText($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
]);
}
}

View File

@@ -1,31 +1,31 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use Illuminate\Http\Response;
use Views;
class ChapterController extends Controller
{
protected $bookRepo;
protected $chapterRepo;
protected $userRepo;
protected $entityRepo;
protected $exportService;
/**
* ChapterController constructor.
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@@ -36,9 +36,9 @@ class ChapterController extends Controller
*/
public function create($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle('Create New Chapter');
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]);
}
@@ -54,12 +54,12 @@ class ChapterController extends Controller
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$input = $request->all();
$input['priority'] = $this->bookRepo->getNewPriority($book);
$chapter = $this->chapterRepo->createFromInput($input, $book);
$input['priority'] = $this->entityRepo->getNewBookPriority($book);
$chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl());
}
@@ -72,15 +72,14 @@ class ChapterController extends Controller
*/
public function show($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = $this->bookRepo->getChildren($book);
$sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
$pages = $this->chapterRepo->getChildren($chapter);
$pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [
'book' => $book,
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
@@ -96,11 +95,10 @@ class ChapterController extends Controller
*/
public function edit($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -112,14 +110,15 @@ class ChapterController extends Controller
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
if ($chapter->name !== $request->get('name')) {
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
}
$chapter->fill($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
$chapter->updated_by = auth()->user()->id;
$chapter->updated_by = user()->id;
$chapter->save();
Activity::add($chapter, 'chapter_update', $book->id);
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
@@ -131,11 +130,10 @@ class ChapterController extends Controller
*/
public function showDelete($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -146,11 +144,11 @@ class ChapterController extends Controller
*/
public function destroy($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
$this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter);
$this->entityRepo->destroyChapter($chapter);
return redirect($book->getUrl());
}
@@ -162,12 +160,12 @@ class ChapterController extends Controller
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [
'chapter' => $chapter,
'book' => $book
'book' => $chapter->book
]);
}
@@ -180,8 +178,7 @@ class ChapterController extends Controller
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$entitySelection = $request->get('entity_selection', null);
@@ -196,17 +193,17 @@ class ChapterController extends Controller
$parent = false;
if ($entityType == 'book') {
$parent = $this->bookRepo->getById($entityId);
$parent = $this->entityRepo->getById('book', $entityId);
}
if ($parent === false || $parent === null) {
session()->flash('The selected Book was not found');
session()->flash('error', trans('errors.selected_book_not_found'));
return redirect()->back();
}
$this->chapterRepo->changeBook($parent->id, $chapter, true);
$this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
return redirect($chapter->getUrl());
}
@@ -219,8 +216,7 @@ class ChapterController extends Controller
*/
public function showRestrict($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
@@ -238,11 +234,58 @@ class ChapterController extends Controller
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
session()->flash('success', 'Chapter Restrictions Updated');
$this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
session()->flash('success', trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
}
/**
* Exports a chapter to pdf .
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
}
/**
* Export a chapter to a self-contained HTML file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
}
/**
* Export a chapter to a simple plaintext .txt file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
}
}

View File

@@ -3,13 +3,11 @@
namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use HttpRequestException;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use BookStack\User;
abstract class Controller extends BaseController
@@ -30,17 +28,21 @@ abstract class Controller extends BaseController
*/
public function __construct()
{
// Get a user instance for the current user
$user = auth()->user();
if (!$user) $user = User::getDefault();
$this->middleware(function ($request, $next) {
// Share variables with views
view()->share('signedIn', auth()->check());
view()->share('currentUser', $user);
// Get a user instance for the current user
$user = user();
// Share variables with controllers
$this->currentUser = $user;
$this->signedIn = auth()->check();
// Share variables with controllers
$this->currentUser = $user;
$this->signedIn = auth()->check();
// Share variables with views
view()->share('signedIn', $this->signedIn);
view()->share('currentUser', $user);
return $next($request);
});
}
/**
@@ -67,8 +69,13 @@ abstract class Controller extends BaseController
*/
protected function showPermissionError()
{
Session::flash('error', trans('errors.permission'));
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
if (request()->wantsJson()) {
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
} else {
$response = redirect('/');
session()->flash('error', trans('errors.permission'));
}
throw new HttpResponseException($response);
}
@@ -79,7 +86,7 @@ abstract class Controller extends BaseController
*/
protected function checkPermission($permissionName)
{
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
if (!user() || !user()->can($permissionName)) {
$this->showPermissionError();
}
return true;
@@ -121,4 +128,22 @@ abstract class Controller extends BaseController
return response()->json(['message' => $messageText], $statusCode);
}
/**
* Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return response()->json(['validation' => $errors], 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Http\Requests;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
@@ -31,9 +32,9 @@ class HomeController extends Controller
$activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [
'activity' => $activity,
'recents' => $recents,
@@ -43,4 +44,39 @@ class HomeController extends Controller
]);
}
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getTranslations() {
$locale = trans()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
} else {
$translations = [
// Get only translations which might be used in JS
'common' => trans('common'),
'components' => trans('components'),
'entities' => trans('entities'),
'errors' => trans('errors')
];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], null, 'en'),
'components' => trans('components', [], null, 'en'),
'entities' => trans('entities', [], null, 'en'),
'errors' => trans('errors', [], null, 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120);
}
return response($resp, 200, [
'Content-Type' => 'application/javascript'
]);
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
@@ -73,6 +74,7 @@ class ImageController extends Controller
* @param $filter
* @param int $page
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getGalleryFiltered($filter, $page = 0, Request $request)
{
@@ -149,12 +151,12 @@ class ImageController extends Controller
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo
* @param EntityRepo $entityRepo
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(PageRepo $pageRepo, Request $request, $id)
public function destroy(EntityRepo $entityRepo, Request $request, $id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
@@ -162,14 +164,14 @@ class ImageController extends Controller
// Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
if (!$isForced) {
$pageSearch = $pageRepo->searchForImage($image->url);
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {
return response()->json($pageSearch, 400);
}
}
$this->imageRepo->destroyImage($image);
return response()->json('Image Deleted');
return response()->json(trans('components.images_deleted'));
}

View File

@@ -2,39 +2,31 @@
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Http\Response;
use Views;
use GatherContent\Htmldiff\Htmldiff;
class PageController extends Controller
{
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $entityRepo;
protected $exportService;
protected $userRepo;
/**
* PageController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param ExportService $exportService
* @param UserRepo $userRepo
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->exportService = $exportService;
$this->userRepo = $userRepo;
parent::__construct();
@@ -42,43 +34,82 @@ class PageController extends Controller
/**
* Show the form for creating a new page.
* @param $bookSlug
* @param bool $chapterSlug
* @param string $bookSlug
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
*/
public function create($bookSlug, $chapterSlug = false)
public function create($bookSlug, $chapterSlug = null)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$this->setPageTitle('Create New Page');
$draft = $this->pageRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl());
// Redirect to draft edit screen if signed in
if ($this->signedIn) {
$draft = $this->entityRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl());
}
// Otherwise show edit view
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
/**
* Create a new page as a guest user.
* @param Request $request
* @param string $bookSlug
* @param string|null $chapterSlug
* @return mixed
* @throws NotFoundException
*/
public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$page = $this->entityRepo->getDraftPage($book, $chapter);
$this->entityRepo->publishPageDraft($page, [
'name' => $request->get('name'),
'html' => ''
]);
return redirect($page->getUrl('/edit'));
}
/**
* Show form to continue editing a draft page.
* @param $bookSlug
* @param $pageId
* @param string $bookSlug
* @param int $pageId
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function editDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$draft = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-create', $book);
$this->setPageTitle('Edit Page Draft');
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->setPageTitle(trans('entities.pages_edit_draft'));
return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
'draftsEnabled' => $draftsEnabled
]);
}
/**
* Store a new page by changing a draft into a page.
* @param Request $request
* @param string $bookSlug
* @param int $pageId
* @return Response
*/
public function store(Request $request, $bookSlug, $pageId)
@@ -88,21 +119,21 @@ class PageController extends Controller
]);
$input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->pageRepo->getById($pageId, true);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
} else {
$input['priority'] = $this->bookRepo->getNewPriority($parent);
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
}
$page = $this->pageRepo->publishDraft($draftPage, $input);
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
@@ -110,83 +141,91 @@ class PageController extends Controller
/**
* Display the specified page.
* If the page is not found via the slug the
* revisions are searched for a match.
* @param $bookSlug
* @param $pageSlug
* If the page is not found via the slug the revisions are searched for a match.
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function show($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$sidebarTree = $this->bookRepo->getChildren($book);
$pageContent = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
}
/**
* Get page from an ajax request.
* @param $pageId
* @param int $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function getPageAjax($pageId)
{
$page = $this->pageRepo->getById($pageId);
$page = $this->entityRepo->getById('page', $pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function edit($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Editing Page ' . $page->getShortName());
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
$page->isDraft = false;
// Check for active editing
$warnings = [];
if ($this->pageRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
if ($this->entityRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
}
// Check for a current draft version for this user
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
$page->name = $draft->name;
$page->html = $draft->html;
$page->markdown = $draft->markdown;
$page->isDraft = true;
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
'draftsEnabled' => $draftsEnabled
]);
}
/**
* Update the specified page in storage.
* @param Request $request
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function update(Request $request, $bookSlug, $pageSlug)
@@ -194,35 +233,38 @@ class PageController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->updatePage($page, $book->id, $request->all());
Activity::add($page, 'page_update', $book->id);
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
Activity::add($page, 'page_update', $page->book->id);
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
* @param Request $request
* @param $pageId
* @param int $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function saveDraft(Request $request, $pageId)
{
$page = $this->pageRepo->getById($pageId, true);
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
if ($page->draft) {
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
} else {
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
if (!$this->signedIn) {
return response()->json([
'status' => 'error',
'message' => trans('errors.guests_cannot_save_drafts'),
], 500);
}
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => 'Draft saved at ',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $utcUpdateTimestamp
]);
}
@@ -230,141 +272,172 @@ class PageController extends Controller
/**
* Redirect from a special link url which
* uses the page id rather than the name.
* @param $pageId
* @param int $pageId
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function redirectFromLink($pageId)
{
$page = $this->pageRepo->getById($pageId);
$page = $this->entityRepo->getById('page', $pageId);
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
*/
public function showDelete($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle('Delete Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
* Show the deletion page for the specified page.
* @param $bookSlug
* @param $pageId
* @param string $bookSlug
* @param int $pageId
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showDeleteDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
* Remove the specified page from storage.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @internal param int $id
*/
public function destroy($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$book = $page->book;
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', 'Page deleted');
$this->pageRepo->destroy($page);
session()->flash('success', trans('entities.pages_delete_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
* @param $bookSlug
* @param $pageId
* @param string $bookSlug
* @param int $pageId
* @return Response
* @throws NotFoundException
*/
public function destroyDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$page = $this->entityRepo->getById('page', $pageId, true);
$book = $page->book;
$this->checkOwnablePermission('page-update', $page);
session()->flash('success', 'Draft deleted');
$this->pageRepo->destroy($page);
session()->flash('success', trans('entities.pages_delete_draft_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
/**
* Shows the last revisions for this page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
*/
public function showRevisions($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->setPageTitle('Revisions For ' . $page->getShortName());
return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
* Shows a preview of a single revision
* @param $bookSlug
* @param $pageSlug
* @param $revisionId
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\View\View
*/
public function showRevision($bookSlug, $pageSlug, $revisionId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$revision = $this->pageRepo->getRevisionById($revisionId);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$page->fill($revision->toArray());
$this->setPageTitle('Page Revision For ' . $page->getShortName());
return view('pages/revision', ['page' => $page, 'book' => $book]);
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
]);
}
/**
* Shows the changes of a single revision
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\View\View
*/
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$prev = $revision->getPrevious();
$prevContent = ($prev === null) ? '' : $prev->html;
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
]);
}
/**
* Restores a page using the content of the specified revision.
* @param $bookSlug
* @param $pageSlug
* @param $revisionId
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
Activity::add($page, 'page_restore', $book->id);
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
Activity::add($page, 'page_restore', $page->book->id);
return redirect($page->getUrl());
}
/**
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
@@ -374,14 +447,13 @@ class PageController extends Controller
/**
* Export a page to a self-contained HTML file.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
@@ -391,14 +463,13 @@ class PageController extends Controller
/**
* Export a page to a simple plaintext .txt file.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
@@ -412,9 +483,9 @@ class PageController extends Controller
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created'));
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => 'Recently Created Pages',
'title' => trans('entities.recently_created_pages'),
'pages' => $pages
]);
}
@@ -425,23 +496,22 @@ class PageController extends Controller
*/
public function showRecentlyUpdated()
{
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [
'title' => 'Recently Updated Pages',
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
@@ -452,34 +522,32 @@ class PageController extends Controller
/**
* Show the view to choose a new parent to move a page into.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showMove($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
return view('pages/move', [
'book' => $book,
'book' => $page->book,
'page' => $page
]);
}
/**
* Does the action of moving the location of a page
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function move($bookSlug, $pageSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
@@ -491,40 +559,34 @@ class PageController extends Controller
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
$parent = false;
if ($entityType == 'chapter') {
$parent = $this->chapterRepo->getById($entityId);
} else if ($entityType == 'book') {
$parent = $this->bookRepo->getById($entityId);
}
if ($parent === false || $parent === null) {
session()->flash('The selected Book or Chapter was not found');
try {
$parent = $this->entityRepo->getById($entityType, $entityId);
} catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back();
}
$this->pageRepo->changePageParent($page, $parent);
$this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
/**
* Set the permissions for this page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', 'Page Permissions Updated');
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}

View File

@@ -2,9 +2,7 @@
use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
use BookStack\Services\PermissionService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class PermissionController extends Controller
{
@@ -55,7 +53,7 @@ class PermissionController extends Controller
]);
$this->permissionsRepo->saveNewRole($request->all());
session()->flash('success', 'Role successfully created');
session()->flash('success', trans('settings.role_create_success'));
return redirect('/settings/roles');
}
@@ -69,7 +67,7 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) throw new PermissionsException('This role cannot be edited');
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
return view('settings/roles/edit', ['role' => $role]);
}
@@ -88,7 +86,7 @@ class PermissionController extends Controller
]);
$this->permissionsRepo->updateRole($id, $request->all());
session()->flash('success', 'Role successfully updated');
session()->flash('success', trans('settings.role_update_success'));
return redirect('/settings/roles');
}
@@ -103,7 +101,7 @@ class PermissionController extends Controller
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
}
@@ -126,7 +124,7 @@ class PermissionController extends Controller
return redirect()->back();
}
session()->flash('success', 'Role successfully deleted');
session()->flash('success', trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}

View File

@@ -1,34 +1,22 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
class SearchController extends Controller
{
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $entityRepo;
protected $viewService;
/**
* SearchController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
parent::__construct();
}
@@ -46,10 +34,10 @@ class SearchController extends Controller
}
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
$this->setPageTitle('Search For ' . $searchTerm);
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
return view('search/all', [
'pages' => $pages,
'books' => $books,
@@ -69,11 +57,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Page Search For ' . $searchTerm);
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $pages,
'title' => 'Page Search Results',
'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm
]);
}
@@ -89,11 +77,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Chapter Search For ' . $searchTerm);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => 'Chapter Search Results',
'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm
]);
}
@@ -109,11 +97,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
$this->setPageTitle('Book Search For ' . $searchTerm);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $books,
'title' => 'Book Search Results',
'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm
]);
}
@@ -132,8 +120,8 @@ class SearchController extends Controller
}
$searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
}
@@ -152,9 +140,11 @@ class SearchController extends Controller
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains($entityType)) {
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
} else {
$entityNames = $entityTypes->map(function ($type) {

View File

@@ -1,8 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use Illuminate\Http\Response;
use Setting;
class SettingController extends Controller
@@ -17,10 +16,7 @@ class SettingController extends Controller
$this->setPageTitle('Settings');
// Get application version
$version = false;
if (function_exists('exec')) {
$version = exec('git describe --always --tags ');
}
$version = trim(file_get_contents(base_path('version')));
return view('settings/index', ['version' => $version]);
}
@@ -42,7 +38,7 @@ class SettingController extends Controller
Setting::put($key, $value);
}
session()->flash('success', 'Settings Saved');
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}

View File

@@ -2,7 +2,6 @@
use BookStack\Repos\TagRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class TagController extends Controller
{
@@ -16,12 +15,14 @@ class TagController extends Controller
public function __construct(TagRepo $tagRepo)
{
$this->tagRepo = $tagRepo;
parent::__construct();
}
/**
* Get all the Tags for a particular entity
* @param $entityType
* @param $entityId
* @return \Illuminate\Http\JsonResponse
*/
public function getForEntity($entityType, $entityId)
{
@@ -29,29 +30,10 @@ class TagController extends Controller
return response()->json($tags);
}
/**
* Update the tags for a particular entity.
* @param $entityType
* @param $entityId
* @param Request $request
* @return mixed
*/
public function updateForEntity($entityType, $entityId, Request $request)
{
$entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
if ($entity === null) return $this->jsonError("Entity not found", 404);
$inputTags = $request->input('tags');
$tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
return response()->json([
'tags' => $tags,
'message' => 'Tags successfully updated'
]);
}
/**
* Get tag name suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getNameSuggestions(Request $request)
{
@@ -63,6 +45,7 @@ class TagController extends Controller
/**
* Get tag value suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getValueSuggestions(Request $request)
{

View File

@@ -1,12 +1,8 @@
<?php
<?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use BookStack\Activity;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use BookStack\Http\Requests;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use BookStack\User;
@@ -43,7 +39,7 @@ class UserController extends Controller
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle('Users');
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
}
@@ -56,7 +52,7 @@ class UserController extends Controller
{
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAssignableRoles();
$roles = $this->userRepo->getAllRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
@@ -82,7 +78,6 @@ class UserController extends Controller
}
$this->validate($request, $validationRules);
$user = $this->user->fill($request->all());
if ($authMethod === 'standard') {
@@ -100,9 +95,14 @@ class UserController extends Controller
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
}
}
return redirect('/settings/users');
@@ -120,12 +120,13 @@ class UserController extends Controller
return $this->currentUser->id == $id;
});
$authMethod = config('auth.method');
$user = $this->user->findOrFail($id);
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle('User Profile');
$roles = $this->userRepo->getAssignableRoles();
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
@@ -146,9 +147,8 @@ class UserController extends Controller
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password'
], [
'password-confirm.required_with' => 'Password confirmation required'
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
]);
$user = $this->user->findOrFail($id);
@@ -171,8 +171,15 @@ class UserController extends Controller
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
if ($request->has('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}
}
$user->save();
session()->flash('success', 'User successfully updated');
session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
return redirect($redirectUrl);
@@ -180,7 +187,7 @@ class UserController extends Controller
/**
* Show the user delete page.
* @param $id
* @param int $id
* @return \Illuminate\View\View
*/
public function delete($id)
@@ -190,7 +197,7 @@ class UserController extends Controller
});
$user = $this->user->findOrFail($id);
$this->setPageTitle('Delete User ' . $user->name);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
}
@@ -209,12 +216,17 @@ class UserController extends Controller
$user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) {
session()->flash('error', 'You cannot delete the only admin');
session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
if ($user->system_name === 'public') {
session()->flash('error', trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
session()->flash('success', 'User successfully removed');
session()->flash('success', trans('settings.users_delete_success'));
return redirect('/settings/users');
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Http;
<?php namespace BookStack\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -9,15 +7,33 @@ class Kernel extends HttpKernel
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
@@ -26,6 +42,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth' => \BookStack\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,

View File

@@ -4,8 +4,6 @@ namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use BookStack\Exceptions\UserRegistrationException;
use Setting;
class Authenticate
{
@@ -33,7 +31,7 @@ class Authenticate
public function handle($request, Closure $next)
{
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect()->guest(baseUrl('/register/confirm/awaiting'));
return redirect(baseUrl('/register/confirm/awaiting'));
}
if ($this->auth->guest() && !setting('app-public')) {

View File

@@ -0,0 +1,23 @@
<?php namespace BookStack\Http\Middleware;
use Carbon\Carbon;
use Closure;
class Localization
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
$locale = setting()->getUser(user(), 'language', $defaultLang);
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Http\Middleware;
<?php namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
@@ -34,7 +32,8 @@ class RedirectIfAuthenticated
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
$requireConfirmation = setting('registration-confirmation');
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
return redirect('/');
}

View File

@@ -1,21 +0,0 @@
<?php
namespace BookStack\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use Queueable;
}

View File

View File

@@ -0,0 +1,49 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification
{
public $token;
/**
* Create a new notification instance.
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$appName = ['appName' => setting('app-name')];
return (new MailMessage)
->subject(trans('auth.email_confirm_subject', $appName))
->greeting(trans('auth.email_confirm_greeting', $appName))
->line(trans('auth.email_confirm_text'))
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPassword extends Notification
{
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* Create a notification instance.
*
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
return (new MailMessage)
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
->line(trans('auth.email_reset_text'))
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
->line(trans('auth.email_reset_not_requested'));
}
}

View File

@@ -7,6 +7,10 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
protected $fieldsToSearch = ['name', 'text'];
/**
* Converts this page into a simplified array.
* @return mixed
@@ -54,6 +58,15 @@ class Page extends Entity
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
}
/**
* Get the attachments assigned to this page.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function attachments()
{
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
}
/**
* Get the url for this page.
* @param string|bool $path
@@ -63,13 +76,13 @@ class Page extends Entity
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : $this->slug;
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
if ($path !== false) {
return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/'));
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $bookSlug . $midText . $idComponent);
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
}
/**

View File

@@ -25,11 +25,26 @@ class PageRevision extends Model
/**
* Get the url for this revision.
* @param null|string $path
* @return string
*/
public function getUrl()
public function getUrl($path = null)
{
return $this->page->getUrl() . '/revisions/' . $this->id;
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) return $url . '/' . trim($path, '/');
return $url;
}
/**
* Get the previous revision for the same page if existing
* @return \BookStack\PageRevision|null
*/
public function getPrevious()
{
if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
return static::find($id);
}
return null;
}
}

View File

@@ -1,6 +1,9 @@
<?php namespace BookStack\Providers;
use BookStack\Services\SettingService;
use BookStack\Setting;
use Illuminate\Support\ServiceProvider;
use Validator;
class AppServiceProvider extends ServiceProvider
{
@@ -12,11 +15,14 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
\Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
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);
});
\Blade::directive('icon', function($expression) {
return "<?php echo icon($expression); ?>";
});
}
/**
@@ -26,6 +32,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(SettingService::class, function($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Broadcast::routes();
//
// /*
// * Authenticate the user's personal channel...
// */
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
// return (int) $user->id === (int) $userId;
// });
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class EventServiceProvider extends ServiceProvider
{
@@ -13,21 +14,18 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'BookStack\Events\SomeEvent' => [
'BookStack\Listeners\EventListener',
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
],
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
public function boot()
{
parent::boot($events);
//
parent::boot();
}
}

View File

@@ -1,11 +1,12 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
use Illuminate\Pagination\Paginator;
class PaginationServiceProvider extends ServiceProvider
class PaginationServiceProvider extends IlluminatePaginationServiceProvider
{
/**
* Register the service provider.
*
@@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider
*/
public function register()
{
Paginator::viewFactoryResolver(function () {
return $this->app['view'];
});
Paginator::currentPathResolver(function () {
return baseUrl($this->app['request']->path());
});

View File

@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Route;
class RouteServiceProvider extends ServiceProvider
{
@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
/**
* Define your route model bindings, pattern filters, etc.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
public function boot()
{
//
parent::boot($router);
parent::boot();
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function map(Router $router)
public function map()
{
$router->group(['namespace' => $this->namespace], function ($router) {
require app_path('Http/routes.php');
$this->mapWebRoutes();
// $this->mapApiRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::group([
'middleware' => 'api',
'namespace' => $this->namespace,
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
});
}
}

View File

@@ -1,36 +0,0 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
class SocialiteServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
return new SocialiteManager($app);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['Laravel\Socialite\Contracts\Factory'];
}
}

View File

@@ -1,294 +0,0 @@
<?php namespace BookStack\Repos;
use Alpha\B;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
class BookRepo extends EntityRepo
{
protected $pageRepo;
protected $chapterRepo;
/**
* BookRepo constructor.
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
*/
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
{
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Base query for getting books.
* Takes into account any restrictions.
* @return mixed
*/
private function bookQuery()
{
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
}
/**
* Get the book that has the given id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->bookQuery()->findOrFail($id);
}
/**
* Get all books, Limited by count.
* @param int $count
* @return mixed
*/
public function getAll($count = 10)
{
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
if (!$count) return $bookQuery->get();
return $bookQuery->take($count)->get();
}
/**
* Get all books paginated.
* @param int $count
* @return mixed
*/
public function getAllPaginated($count = 10)
{
return $this->bookQuery()
->orderBy('name', 'asc')->paginate($count);
}
/**
* Get the latest books.
* @param int $count
* @return mixed
*/
public function getLatest($count = 10)
{
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
}
/**
* Gets the most recently viewed for a user.
* @param int $count
* @param int $page
* @return mixed
*/
public function getRecentlyViewed($count = 10, $page = 0)
{
return Views::getUserRecentlyViewed($count, $page, $this->book);
}
/**
* Gets the most viewed books.
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular($count = 10, $page = 0)
{
return Views::getPopular($count, $page, $this->book);
}
/**
* Get a book by slug
* @param $slug
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
/**
* Checks if a book exists.
* @param $id
* @return bool
*/
public function exists($id)
{
return $this->bookQuery()->where('id', '=', $id)->exists();
}
/**
* Get a new book instance from request input.
* @param array $input
* @return Book
*/
public function createFromInput($input)
{
$book = $this->book->newInstance($input);
$book->slug = $this->findSuitableSlug($book->name);
$book->created_by = auth()->user()->id;
$book->updated_by = auth()->user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Update the given book from user input.
* @param Book $book
* @param $input
* @return Book
*/
public function updateFromInput(Book $book, $input)
{
$book->fill($input);
$book->slug = $this->findSuitableSlug($book->name, $book->id);
$book->updated_by = auth()->user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Destroy the given book.
* @param Book $book
* @throws \Exception
*/
public function destroy(Book $book)
{
foreach ($book->pages as $page) {
$this->pageRepo->destroy($page);
}
foreach ($book->chapters as $chapter) {
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$book->delete();
}
/**
* Get the next child element priority.
* @param Book $book
* @return int
*/
public function getNewPriority($book)
{
$lastElem = $this->getChildren($book)->pop();
return $lastElem ? $lastElem->priority + 1 : 0;
}
/**
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $currentId = false)
{
$query = $this->book->where('slug', '=', $slug);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Provides a suitable slug for the given book name.
* Ensures the returned slug is unique in the system.
* @param string $name
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $currentId = false)
{
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @return mixed
*/
public function getChildren(Book $book, $filterDrafts = false)
{
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
if ($filterDrafts) {
$pageQuery = $pageQuery->where('draft', '=', false);
}
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
$this->permissionService->enforcePageRestrictions($query, 'view');
if ($filterDrafts) $query->where('draft', '=', false);
}]);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->values();
foreach ($chapters as $chapter) {
$children->push($chapter);
}
$bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
$child->pages = $child->pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
});
// Sort items with drafts first then by priority.
return $children->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->isA('page') && $child->draft) $score -= 100;
return $score;
});
}
/**
* Get books by search term.
* @param $term
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
$books = $bookQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
$book->searchSnippet = $result;
}
return $books;
}
}

View File

@@ -1,227 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Chapter;
class ChapterRepo extends EntityRepo
{
protected $pageRepo;
/**
* ChapterRepo constructor.
* @param $pageRepo
*/
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
parent::__construct();
}
/**
* Base query for getting chapters, Takes permissions into account.
* @return mixed
*/
private function chapterQuery()
{
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
}
/**
* Check if an id exists.
* @param $id
* @return bool
*/
public function idExists($id)
{
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
}
/**
* Get a chapter by a specific id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->chapterQuery()->findOrFail($id);
}
/**
* Get all chapters.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->chapterQuery()->all();
}
/**
* Get a chapter that has the given slug within the given book.
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter;
}
/**
* Get the child items for a chapter
* @param Chapter $chapter
*/
public function getChildren(Chapter $chapter)
{
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
// Sort items with drafts first then by priority.
return $pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
/**
* Create a new chapter from request input.
* @param $input
* @param Book $book
* @return Chapter
*/
public function createFromInput($input, Book $book)
{
$chapter = $this->chapter->newInstance($input);
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
$chapter->created_by = auth()->user()->id;
$chapter->updated_by = auth()->user()->id;
$chapter = $book->chapters()->save($chapter);
$this->permissionService->buildJointPermissionsForEntity($chapter);
return $chapter;
}
/**
* Destroy a chapter and its relations by providing its slug.
* @param Chapter $chapter
*/
public function destroy(Chapter $chapter)
{
if (count($chapter->pages) > 0) {
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->save();
}
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$chapter->delete();
}
/**
* Check if a chapter's slug exists.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Finds a suitable slug for the provided name.
* Checks database to prevent duplicate slugs.
* @param $name
* @param $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get a new priority value for a new page to be added
* to the given chapter.
* @param Chapter $chapter
* @return int
*/
public function getNewPriority(Chapter $chapter)
{
$lastPage = $chapter->pages->last();
return $lastPage !== null ? $lastPage->priority + 1 : 0;
}
/**
* Get chapters by the given search term.
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
$chapter->searchSnippet = $result;
}
return $chapters;
}
/**
* Changes the book relation of this chapter.
* @param $bookId
* @param Chapter $chapter
* @param bool $rebuildPermissions
* @return Chapter
*/
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
{
$chapter->book_id = $bookId;
// Update related activity
foreach ($chapter->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
$chapter->save();
// Update all child pages
foreach ($chapter->pages as $page) {
$this->pageRepo->changeBook($bookId, $page);
}
// Update permissions if applicable
if ($rebuildPermissions) {
$chapter->load('book');
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
}
return $chapter;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -191,7 +192,12 @@ class ImageRepo
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
} catch (FileNotFoundException $exception) {
$image->delete();
return [];
}
}

View File

@@ -1,650 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use Carbon\Carbon;
use DOMDocument;
use Illuminate\Support\Str;
use BookStack\Page;
use BookStack\PageRevision;
class PageRepo extends EntityRepo
{
protected $pageRevision;
protected $tagRepo;
/**
* PageRepo constructor.
* @param PageRevision $pageRevision
* @param TagRepo $tagRepo
*/
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
{
$this->pageRevision = $pageRevision;
$this->tagRepo = $tagRepo;
parent::__construct();
}
/**
* Base query for getting pages, Takes restrictions into account.
* @param bool $allowDrafts
* @return mixed
*/
private function pageQuery($allowDrafts = false)
{
$query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
if (!$allowDrafts) {
$query = $query->where('draft', '=', false);
}
return $query;
}
/**
* Get a page via a specific ID.
* @param $id
* @param bool $allowDrafts
* @return mixed
*/
public function getById($id, $allowDrafts = false)
{
return $this->pageQuery($allowDrafts)->findOrFail($id);
}
/**
* Get a page identified by the given slug.
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) throw new NotFoundException('Page not found');
return $page;
}
/**
* Search through page revisions and retrieve
* the last page in the current book that
* has a slug equal to the one given.
* @param $pageSlug
* @param $bookSlug
* @return null | Page
*/
public function findPageUsingOldSlug($pageSlug, $bookSlug)
{
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
->whereHas('page', function ($query) {
$this->permissionService->enforcePageRestrictions($query);
})
->where('type', '=', 'version')
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
->with('page')->first();
return $revision !== null ? $revision->page : null;
}
/**
* Get a new Page instance from the given input.
* @param $input
* @return Page
*/
public function newFromInput($input)
{
$page = $this->page->fill($input);
return $page;
}
/**
* Count the pages with a particular slug within a book.
* @param $slug
* @param $bookId
* @return mixed
*/
public function countBySlug($slug, $bookId)
{
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
}
/**
* Save a new page into the system.
* Input validation must be done beforehand.
* @param array $input
* @param Book $book
* @param int $chapterId
* @return Page
*/
public function saveNew(array $input, Book $book, $chapterId = null)
{
$page = $this->newFromInput($input);
$page->slug = $this->findSuitableSlug($page->name, $book->id);
if ($chapterId) $page->chapter_id = $chapterId;
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->created_by = auth()->user()->id;
$page->updated_by = auth()->user()->id;
$book->pages()->save($page);
return $page;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
* @param Page $draftPage
* @param array $input
* @return Page
*/
public function publishDraft(Page $draftPage, array $input)
{
$draftPage->fill($input);
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
}
$draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->draft = false;
$draftPage->save();
$this->saveRevision($draftPage, 'Initial Publish');
return $draftPage;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return static
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = 'New Page';
$page->created_by = auth()->user()->id;
$page->updated_by = auth()->user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* @param string $htmlText
* @return string
*/
protected function formatHtml($htmlText)
{
if ($htmlText == '') return $htmlText;
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Gets pages by a search term.
* Highlights page content for showing in results.
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
// Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
//lookahead/behind assertions ensures cut between words
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
foreach ($pages as $page) {
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
//delimiter between occurrences
$results = [];
foreach ($matches as $line) {
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
}
$matchLimit = 6;
if (count($results) > $matchLimit) {
$results = array_slice($results, 0, $matchLimit);
}
$result = join('... ', $results);
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
if (strlen($result) < 5) {
$result = $page->getExcerpt(80);
}
$page->searchSnippet = $result;
}
return $pages;
}
/**
* Search for image usage.
* @param $imageString
* @return mixed
*/
public function searchForImage($imageString)
{
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
$page->text = '';
}
return count($pages) > 0 ? $pages : false;
}
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param string $input
* @return Page
*/
public function updatePage(Page $page, $book_id, $input)
{
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
}
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
}
// Update with new details
$userId = auth()->user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId;
$page->save();
// Remove all update drafts for this user & page.
$this->userUpdateDraftsQuery($page, $userId)->delete();
// Save a revision after updating
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
$this->saveRevision($page, $input['summary']);
}
return $page;
}
/**
* Restores a revision's content back into a page.
* @param Page $page
* @param Book $book
* @param int $revisionId
* @return Page
*/
public function restoreRevision(Page $page, Book $book, $revisionId)
{
$this->saveRevision($page);
$revision = $this->getRevisionById($revisionId);
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
$page->text = strip_tags($page->html);
$page->updated_by = auth()->user()->id;
$page->save();
return $page;
}
/**
* Saves a page revision into the system.
* @param Page $page
* @param null|string $summary
* @return $this
*/
public function saveRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->fill($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
$revision->created_by = auth()->user()->id;
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
$this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
}
return $revision;
}
/**
* Save a page update draft.
* @param Page $page
* @param array $data
* @return PageRevision
*/
public function saveUpdateDraft(Page $page, $data = [])
{
$userId = auth()->user()->id;
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
if ($drafts->count() > 0) {
$draft = $drafts->first();
} else {
$draft = $this->pageRevision->newInstance();
$draft->page_id = $page->id;
$draft->slug = $page->slug;
$draft->book_slug = $page->book->slug;
$draft->created_by = $userId;
$draft->type = 'update_draft';
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
$draft->save();
return $draft;
}
/**
* Update a draft page.
* @param Page $page
* @param array $data
* @return Page
*/
public function updateDraftPage(Page $page, $data = [])
{
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
}
$page->save();
return $page;
}
/**
* The base query for getting user update drafts.
* @param Page $page
* @param $userId
* @return mixed
*/
private function userUpdateDraftsQuery(Page $page, $userId)
{
return $this->pageRevision->where('created_by', '=', $userId)
->where('type', 'update_draft')
->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc');
}
/**
* Checks whether a user has a draft version of a particular page or not.
* @param Page $page
* @param $userId
* @return bool
*/
public function hasUserGotPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
}
/**
* Get the latest updated draft revision for a particular page and user.
* @param Page $page
* @param $userId
* @return mixed
*/
public function getUserPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->first();
}
/**
* Get the notification message that informs the user that they are editing a draft page.
* @param PageRevision $draft
* @return string
*/
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
}
return $message;
}
/**
* Check if a page is being actively editing.
* Checks for edits since last page updated.
* Passing in a minuted range will check for edits
* within the last x minutes.
* @param Page $page
* @param null $minRange
* @return bool
*/
public function isPageEditingActive(Page $page, $minRange = null)
{
$draftSearch = $this->activePageEditingQuery($page, $minRange);
return $draftSearch->count() > 0;
}
/**
* Get a notification message concerning the editing activity on
* a particular page.
* @param Page $page
* @param null $minRange
* @return string
*/
public function getPageEditingActiveMessage(Page $page, $minRange = null)
{
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
return sprintf($message, $userMessage, $timeMessage);
}
/**
* A query to check for active update drafts on a particular page.
* @param Page $page
* @param null $minRange
* @return mixed
*/
private function activePageEditingQuery(Page $page, $minRange = null)
{
$query = $this->pageRevision->where('type', '=', 'update_draft')
->where('page_id', '=', $page->id)
->where('updated_at', '>', $page->updated_at)
->where('created_by', '!=', auth()->user()->id)
->with('createdBy');
if ($minRange !== null) {
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
}
return $query;
}
/**
* Gets a single revision via it's id.
* @param $id
* @return mixed
*/
public function getRevisionById($id)
{
return $this->pageRevision->findOrFail($id);
}
/**
* Checks if a slug exists within a book already.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) $query = $query->where('id', '!=', $currentId);
return $query->count() > 0;
}
/**
* Changes the related book for the specified page.
* Changes the book id of any relations to the page that store the book id.
* @param int $bookId
* @param Page $page
* @return Page
*/
public function changeBook($bookId, Page $page)
{
$page->book_id = $bookId;
foreach ($page->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
$page->save();
return $page;
}
/**
* Change the page's parent to the given entity.
* @param Page $page
* @param Entity $parent
*/
public function changePageParent(Page $page, Entity $parent)
{
$book = $parent->isA('book') ? $parent : $parent->book;
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
$page->save();
$page = $this->changeBook($book->id, $page);
$page->load('book');
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Gets a suitable slug for the resource
* @param string $name
* @param int $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Destroy a given page along with its dependencies.
* @param $page
*/
public function destroy(Page $page)
{
Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$page->delete();
}
/**
* Get the latest pages added to the system.
* @param $count
*/
public function getRecentlyCreatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
}
/**
* Get the latest pages added to the system.
* @param $count
*/
public function getRecentlyUpdatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
}
}

View File

@@ -35,7 +35,7 @@ class PermissionsRepo
*/
public function getAllRoles()
{
return $this->role->where('hidden', '=', false)->get();
return $this->role->all();
}
/**
@@ -45,7 +45,7 @@ class PermissionsRepo
*/
public function getAllRolesExcept(Role $role)
{
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
return $this->role->where('id', '!=', $role->id)->get();
}
/**
@@ -90,12 +90,10 @@ class PermissionsRepo
{
$role = $this->role->findOrFail($roleId);
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->name === 'admin') {
if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
}
@@ -135,9 +133,9 @@ class PermissionsRepo
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
throw new PermissionsException('This role is a system role and cannot be deleted');
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
} else if ($role->id == setting('registration-role')) {
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
if ($migrateRoleId) {

View File

@@ -38,7 +38,7 @@ class TagRepo
{
$entityInstance = $this->entity->getEntityInstance($entityType);
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
$searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
return $searchQuery->first();
}
@@ -121,7 +121,7 @@ class TagRepo
/**
* Create a new Tag instance from user input.
* @param $input
* @return static
* @return Tag
*/
protected function newInstanceFromInput($input)
{

View File

@@ -2,7 +2,7 @@
use BookStack\Role;
use BookStack\User;
use Setting;
use Exception;
class UserRepo
{
@@ -84,9 +84,14 @@ class UserRepo
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
$user->save();
\Log::error('Failed to save user gravatar image');
}
}
return $user;
@@ -163,13 +168,13 @@ class UserRepo
public function getRecentlyCreated(User $user, $count = 20)
{
return [
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
];
@@ -193,9 +198,9 @@ class UserRepo
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAssignableRoles()
public function getAllRoles()
{
return $this->role->visible();
return $this->role->all();
}
/**
@@ -205,7 +210,7 @@ class UserRepo
*/
public function getRestrictableRoles()
{
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
return $this->role->where('system_name', '!=', 'admin')->get();
}
}

View File

@@ -66,7 +66,7 @@ class Role extends Model
/**
* Get the role object for the specified role.
* @param $roleName
* @return mixed
* @return Role
*/
public static function getRole($roleName)
{
@@ -76,7 +76,7 @@ class Role extends Model
/**
* Get the role object for the specified system role.
* @param $roleName
* @return mixed
* @return Role
*/
public static function getSystemRole($roleName)
{

View File

@@ -19,7 +19,7 @@ class ActivityService
{
$this->activity = $activity;
$this->permissionService = $permissionService;
$this->user = auth()->user();
$this->user = user();
}
/**
@@ -114,7 +114,7 @@ class ActivityService
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activity);
}

View File

@@ -0,0 +1,201 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use Exception;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
{
/**
* Get an attachment from storage.
* @param Attachment $attachment
* @return string
*/
public function getAttachmentFromStorage(Attachment $attachment)
{
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
return $this->getStorage()->get($attachmentPath);
}
/**
* Store a new attachment upon user upload.
* @param UploadedFile $uploadedFile
* @param int $page_id
* @return Attachment
* @throws FileUploadException
*/
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
'name' => $attachmentName,
'path' => $attachmentPath,
'extension' => $uploadedFile->getClientOriginalExtension(),
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1
]);
return $attachment;
}
/**
* Store a upload, saving to a file and deleting any existing uploads
* attached to that file.
* @param UploadedFile $uploadedFile
* @param Attachment $attachment
* @return Attachment
* @throws FileUploadException
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
{
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
}
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
$attachment->external = false;
$attachment->extension = $uploadedFile->getClientOriginalExtension();
$attachment->save();
return $attachment;
}
/**
* Save a new File attachment from a given link and name.
* @param string $name
* @param string $link
* @param int $page_id
* @return Attachment
*/
public function saveNewFromLink($name, $link, $page_id)
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
return Attachment::forceCreate([
'name' => $name,
'path' => $link,
'external' => true,
'extension' => '',
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1
]);
}
/**
* Get the file storage base path, amended for storage type.
* This allows us to keep a generic path in the database.
* @return string
*/
private function getStorageBasePath()
{
return $this->isLocal() ? 'storage/' : '';
}
/**
* Updates the file ordering for a listing of attached files.
* @param array $attachmentList
* @param $pageId
*/
public function updateFileOrderWithinPage($attachmentList, $pageId)
{
foreach ($attachmentList as $index => $attachment) {
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
}
}
/**
* Update the details of a file.
* @param Attachment $attachment
* @param $requestData
* @return Attachment
*/
public function updateFile(Attachment $attachment, $requestData)
{
$attachment->name = $requestData['name'];
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
$attachment->path = $requestData['link'];
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
$attachment->external = true;
}
}
$attachment->save();
return $attachment;
}
/**
* Delete a File from the database and storage.
* @param Attachment $attachment
*/
public function deleteFile(Attachment $attachment)
{
if ($attachment->external) {
$attachment->delete();
return;
}
$this->deleteFileInStorage($attachment);
$attachment->delete();
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
* @param Attachment $attachment
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
$storage = $this->getStorage();
$dirPath = dirname($storedFilePath);
$storage->delete($storedFilePath);
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
}
/**
* Store a file in storage with the given filename
* @param $attachmentName
* @param UploadedFile $uploadedFile
* @return string
* @throws FileUploadException
*/
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
$uploadFileName = $attachmentName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$attachmentPath = $attachmentBasePath . $uploadFileName;
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
try {
$storage->put($attachmentStoragePath, $attachmentData);
} catch (Exception $e) {
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
}
return $attachmentPath;
}
}

View File

@@ -1,30 +1,27 @@
<?php namespace BookStack\Services;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Repos\UserRepo;
use Carbon\Carbon;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use BookStack\EmailConfirmation;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Database\Connection as Database;
class EmailConfirmationService
{
protected $mailer;
protected $emailConfirmation;
protected $db;
protected $users;
/**
* EmailConfirmationService constructor.
* @param Mailer $mailer
* @param EmailConfirmation $emailConfirmation
* @param Database $db
* @param UserRepo $users
*/
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
public function __construct(Database $db, UserRepo $users)
{
$this->mailer = $mailer;
$this->emailConfirmation = $emailConfirmation;
$this->db = $db;
$this->users = $users;
}
/**
@@ -36,45 +33,59 @@ class EmailConfirmationService
public function sendConfirmation(User $user)
{
if ($user->email_confirmed) {
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
}
$this->deleteConfirmationsByUser($user);
$token = $this->createEmailConfirmation($user);
$user->notify(new ConfirmEmail($token));
}
/**
* Creates a new email confirmation in the database and returns the token.
* @param User $user
* @return string
*/
public function createEmailConfirmation(User $user)
{
$token = $this->getToken();
$this->emailConfirmation->create([
$this->db->table('email_confirmations')->insert([
'user_id' => $user->id,
'token' => $token,
'token' => $token,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
$appName = setting('app-name', 'BookStack');
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
});
return $token;
}
/**
* Gets an email confirmation by looking up the token,
* Ensures the token has not expired.
* @param string $token
* @return EmailConfirmation
* @return array|null|\stdClass
* @throws UserRegistrationException
*/
public function getEmailConfirmationFromToken($token)
{
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first();
// If not found
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
// If not found show error
if ($emailConfirmation === null) {
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
}
// If more than a day old
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) {
$this->sendConfirmation($emailConfirmation->user);
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
$user = $this->users->getById($emailConfirmation->user_id);
$this->sendConfirmation($user);
throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
}
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
return $emailConfirmation;
}
/**
* Delete all email confirmations that belong to a user.
* @param User $user
@@ -82,7 +93,7 @@ class EmailConfirmationService
*/
public function deleteConfirmationsByUser(User $user)
{
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete();
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
}
/**
@@ -92,7 +103,7 @@ class EmailConfirmationService
protected function getToken()
{
$token = str_random(24);
while ($this->emailConfirmation->where('token', '=', $token)->exists()) {
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
$token = str_random(25);
}
return $token;

View File

@@ -1,11 +1,24 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class ExportService
{
protected $entityRepo;
/**
* ExportService constructor.
* @param $entityRepo
*/
public function __construct(EntityRepo $entityRepo)
{
$this->entityRepo = $entityRepo;
}
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
@@ -14,22 +27,108 @@ class ExportService
*/
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->containHtml($pageHtml);
}
/**
* Convert a page to a pdf file.
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->containHtml($html);
}
/**
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
*/
public function bookToContainedHtml(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->containHtml($html);
}
/**
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
*/
public function pageToPdf(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
$containedHtml = $this->containHtml($pageHtml);
$pdf = \PDF::loadHTML($containedHtml);
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a book to a PDF file
* @param Book $book
* @return string
*/
public function bookToPdf(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
*/
protected function htmlToPdf($html)
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
}
return $pdf->output();
}
@@ -55,9 +154,13 @@ class ExportService
$pathString = $srcString;
}
if ($isLocal && !file_exists($pathString)) continue;
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
try {
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {
$newImageString = '';
}
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
}
}
@@ -84,14 +187,14 @@ class ExportService
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to
* provide a nice final output.
* This method filters any bad looking content to provide a nice final output.
* @param Page $page
* @return mixed
*/
public function pageToPlainText(Page $page)
{
$text = $page->text;
$html = $this->entityRepo->renderPage($page);
$text = strip_tags($html);
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
@@ -102,6 +205,40 @@ class ExportService
return $text;
}
/**
* Convert a chapter into a plain text string.
* @param Chapter $chapter
* @return string
*/
public function chapterToPlainText(Chapter $chapter)
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
}
/**
* Convert a book into a plain text string.
* @param Book $book
* @return string
*/
public function bookToPlainText(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
$text .= $this->chapterToPlainText($bookChild);
} else {
$text .= $this->pageToPlainText($bookChild);
}
}
return $text;
}
}

View File

@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Cache\Repository as Cache;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService
class ImageService extends UploadService
{
protected $imageTool;
protected $fileSystem;
protected $cache;
/**
* @var FileSystemInstance
*/
protected $storageInstance;
protected $storageUrl;
/**
@@ -34,8 +27,8 @@ class ImageService
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->imageTool = $imageTool;
$this->fileSystem = $fileSystem;
$this->cache = $cache;
parent::__construct($fileSystem);
}
/**
@@ -66,7 +59,7 @@ class ImageService
{
$imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url);
if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
return $this->saveNew($imageName, $imageData, $type);
}
@@ -88,6 +81,9 @@ class ImageService
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
}
@@ -97,9 +93,11 @@ class ImageService
$storage->put($fullPath, $imageData);
$storage->setVisibility($fullPath, 'public');
} catch (Exception $e) {
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
}
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
@@ -108,8 +106,8 @@ class ImageService
'uploaded_to' => $uploadedTo
];
if (auth()->user() && auth()->user()->id !== 0) {
$userId = auth()->user()->id;
if (user()->id !== 0) {
$userId = user()->id;
$imageDetails['created_by'] = $userId;
$imageDetails['updated_by'] = $userId;
}
@@ -119,6 +117,16 @@ class ImageService
return $image;
}
/**
* Get the storage path, Dependant of storage type.
* @param Image $image
* @return mixed|string
*/
protected function getPath(Image $image)
{
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
}
/**
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
@@ -135,7 +143,8 @@ class ImageService
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
$thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
$imagePath = $this->getPath($image);
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
@@ -148,10 +157,10 @@ class ImageService
}
try {
$thumb = $this->imageTool->make($storage->get($image->path));
$thumb = $this->imageTool->make($storage->get($imagePath));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
} else {
throw $e;
}
@@ -183,8 +192,8 @@ class ImageService
{
$storage = $this->getStorage();
$imageFolder = dirname($image->path);
$imageFileName = basename($image->path);
$imageFolder = dirname($this->getPath($image));
$imageFileName = basename($this->getPath($image));
$allImages = collect($storage->allFiles($imageFolder));
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
@@ -213,7 +222,7 @@ class ImageService
public function saveUserGravatar(User $user, $size = 500)
{
$emailHash = md5(strtolower(trim($user->email)));
$url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName);
$image->created_by = $user->id;
@@ -222,35 +231,9 @@ class ImageService
return $image;
}
/**
* Get the storage that will be used for storing images.
* @return FileSystemInstance
*/
private function getStorage()
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
}
/**
* Check whether or not a folder is empty.
* @param $path
* @return int
*/
private function isFolderEmpty($path)
{
$files = $this->getStorage()->files($path);
$folders = $this->getStorage()->directories($path);
return count($files) === 0 && count($folders) === 0;
}
/**
* Gets a public facing url for an image by checking relevant environment variables.
* @param $filePath
* @param string $filePath
* @return string
*/
private function getPublicUrl($filePath)
@@ -273,6 +256,8 @@ class ImageService
$this->storageUrl = $storageUrl;
}
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
}

View File

@@ -94,4 +94,4 @@ class Ldap
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}
}

View File

@@ -41,7 +41,8 @@ class LdapService
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
$emailAttr = $this->config['email_attribute'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;
$user = $users[0];
@@ -49,7 +50,7 @@ class LdapService
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}
@@ -94,7 +95,7 @@ class LdapService
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
/**
@@ -109,15 +110,19 @@ class LdapService
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException('LDAP PHP extension not installed');
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Get port from server string if specified.
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) array_unshift($ldapServer, '');
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
throw new LdapException(trans('errors.ldap_cannot_connect'));
}
// Set any required options

View File

@@ -8,20 +8,24 @@ use BookStack\Ownable;
use BookStack\Page;
use BookStack\Role;
use BookStack\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class PermissionService
{
protected $userRoles;
protected $isAdmin;
protected $currentAction;
protected $currentUser;
protected $isAdminUser;
protected $userRoles = false;
protected $currentUserModel = false;
public $book;
public $chapter;
public $page;
protected $db;
protected $jointPermission;
protected $role;
@@ -30,24 +34,21 @@ class PermissionService
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param Connection $db
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
$this->currentUser = auth()->user();
$userSet = $this->currentUser !== null;
$this->userRoles = false;
$this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
if (!$userSet) $this->currentUser = new User();
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->role = $role;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
}
/**
@@ -117,7 +118,7 @@ class PermissionService
}
foreach ($this->currentUser->roles as $role) {
foreach ($this->currentUser()->roles as $role) {
$roles[] = $role->id;
}
return $roles;
@@ -156,7 +157,7 @@ class PermissionService
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$roles = $this->role->with('jointPermissions')->get();
$roles = $this->role->get();
$entities = collect([$entity]);
if ($entity->isA('book')) {
@@ -176,7 +177,7 @@ class PermissionService
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->with('jointPermissions')->get();
$roles = $this->role->get();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
@@ -242,13 +243,14 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
@@ -307,6 +309,10 @@ class PermissionService
$explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction);
if ($role->system_name === 'admin') {
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
}
if ($entity->isA('book')) {
if (!$entity->restricted) {
@@ -389,21 +395,25 @@ class PermissionService
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin) return true;
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions'];
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
$allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
$ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
$this->currentAction = 'view';
$isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
return ($allPermission || ($isOwner && $ownPermission));
}
@@ -412,8 +422,9 @@ class PermissionService
$this->currentAction = $permission;
}
return $this->entityRestrictionQuery($baseQuery)->count() > 0;
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
$this->clean();
return $q;
}
/**
@@ -443,7 +454,7 @@ class PermissionService
*/
protected function entityRestrictionQuery($query)
{
return $query->where(function ($parentQuery) {
$q = $query->where(function ($parentQuery) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getRoles())
->where('action', '=', $this->currentAction)
@@ -451,65 +462,79 @@ class PermissionService
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
->where('created_by', '=', $this->currentUser()->id);
});
});
});
});
$this->clean();
return $q;
}
/**
* Add restrictions for a page query
* @param $query
* @param string $action
* @return mixed
* Get the children of a book in an efficient single query, Filtered by the permission system.
* @param integer $book_id
* @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder
*/
public function enforcePageRestrictions($query, $action = 'view')
{
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
$query->where('draft', '=', false);
if ($this->currentUser) {
$query->orWhere(function ($query) {
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageContentSelect = $fetchPageContent ? 'html' : "''";
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->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);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->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);
return $this->enforceEntityRestrictions($query, $action);
}
if (!$this->isAdmin()) {
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
/**
* Add on permission restrictions to a chapter query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceChapterRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add restrictions to a book query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceBookRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
return $query;
}
/**
* Add restrictions for a generic entity
* @param $query
* @param string $entityType
* @param Builder|Entity $query
* @param string $action
* @return mixed
*/
public function enforceEntityRestrictions($query, $action = 'view')
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{
if ($this->isAdmin) return $query;
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
$query->where('draft', '=', false);
if ($this->currentUser()) {
$query->orWhere(function ($query) {
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
});
}
});
}
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
@@ -524,11 +549,15 @@ class PermissionService
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{
if ($this->isAdmin) return $query;
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
return $query->where(function ($query) use ($tableDetails) {
$q = $query->where(function ($query) use ($tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
@@ -538,12 +567,13 @@ class PermissionService
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
->where('created_by', '=', $this->currentUser()->id);
});
});
});
});
$this->clean();
return $q;
}
/**
@@ -555,11 +585,15 @@ class PermissionService
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin) return $query;
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
return $query->where(function ($query) use ($tableDetails) {
$q = $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
@@ -570,12 +604,50 @@ class PermissionService
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
->where('created_by', '=', $this->currentUser()->id);
});
});
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
return $q;
}
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
}
return $this->isAdminUser;
}
/**
* Get the current user
* @return User
*/
private function currentUser()
{
if ($this->currentUserModel === false) {
$this->currentUserModel = user();
}
return $this->currentUserModel;
}
/**
* Clean the cached user elements.
*/
private function clean()
{
$this->currentUserModel = false;
$this->userRoles = false;
$this->isAdminUser = null;
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
@@ -15,6 +16,7 @@ class SettingService
protected $setting;
protected $cache;
protected $localCache = [];
protected $cachePrefix = 'setting-';
@@ -38,8 +40,25 @@ class SettingService
*/
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
if (isset($this->localCache[$key])) return $this->localCache[$key];
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
return $formatted;
}
/**
* Get a user-specific setting from the database or cache.
* @param User $user
* @param $key
* @param bool $default
* @return bool|string
*/
public function getUser($user, $key, $default = false)
{
return $this->get($this->userKey($user->id, $key), $default);
}
/**
@@ -57,9 +76,8 @@ class SettingService
// Check the cache
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
@@ -69,14 +87,6 @@ class SettingService
return $value;
}
// Check the defaults set in the app config.
$configPrefix = 'setting-defaults.' . $key;
if (config()->has($configPrefix)) {
$value = config($configPrefix);
$this->cache->forever($cacheKey, $value);
return $value;
}
return $default;
}
@@ -118,6 +128,16 @@ class SettingService
return $setting !== null;
}
/**
* Check if a user setting is in the database.
* @param $key
* @return bool
*/
public function hasUser($key)
{
return $this->has($this->userKey($key));
}
/**
* Add a setting to the database.
* @param $key
@@ -135,6 +155,28 @@ class SettingService
return true;
}
/**
* Put a user-specific setting into the database.
* @param User $user
* @param $key
* @param $value
* @return bool
*/
public function putUser($user, $key, $value)
{
return $this->put($this->userKey($user->id, $key), $value);
}
/**
* Convert a setting key into a user-specific key.
* @param $key
* @return string
*/
protected function userKey($userId, $key = '')
{
return 'user:' . $userId . ':' . $key;
}
/**
* Removes a setting from the database.
* @param $key
@@ -150,6 +192,16 @@ class SettingService
return true;
}
/**
* Delete settings for a given user id.
* @param $userId
* @return mixed
*/
public function deleteUserSettings($userId)
{
return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
}
/**
* Gets a setting model from the database for the given key.
* @param $key

View File

@@ -14,7 +14,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
/**
* SocialAuthService constructor.
@@ -70,12 +70,12 @@ class SocialAuthService
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
}
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
$email = $socialUser->getEmail();
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
}
return $socialUser;
@@ -98,9 +98,8 @@ class SocialAuthService
// Get any attached social accounts or users
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$user = $this->userRepo->getByEmail($socialUser->getEmail());
$isLoggedIn = auth()->check();
$currentUser = auth()->user();
$currentUser = user();
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
@@ -113,27 +112,26 @@ class SocialAuthService
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in, A social account exists but the users do not match.
// Change the user that the social account is assigned to.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// Otherwise let the user know this social account is not used by anyone.
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
$message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
if (setting('registration-enabled')) {
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
@@ -157,8 +155,8 @@ class SocialAuthService
{
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
return $driver;
}
@@ -183,14 +181,24 @@ class SocialAuthService
public function getActiveDrivers()
{
$activeDrivers = [];
foreach ($this->validSocialDrivers as $driverName) {
if ($this->checkDriverConfigured($driverName)) {
$activeDrivers[$driverName] = true;
foreach ($this->validSocialDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
}
}
return $activeDrivers;
}
/**
* Get the presentational name for a driver.
* @param $driver
* @return mixed
*/
public function getDriverName($driver)
{
return config('services.' . strtolower($driver) . '.name');
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
@@ -213,10 +221,9 @@ class SocialAuthService
*/
public function detachSocialAccount($socialDriver)
{
session();
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
return redirect(auth()->user()->getEditUrl());
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());
}
}

View File

@@ -0,0 +1,64 @@
<?php namespace BookStack\Services;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
class UploadService
{
/**
* @var FileSystem
*/
protected $fileSystem;
/**
* @var FileSystemInstance
*/
protected $storageInstance;
/**
* FileService constructor.
* @param $fileSystem
*/
public function __construct(FileSystem $fileSystem)
{
$this->fileSystem = $fileSystem;
}
/**
* Get the storage that will be used for storing images.
* @return FileSystemInstance
*/
protected function getStorage()
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
}
/**
* Check whether or not a folder is empty.
* @param $path
* @return bool
*/
protected function isFolderEmpty($path)
{
$files = $this->getStorage()->files($path);
$folders = $this->getStorage()->directories($path);
return (count($files) === 0 && count($folders) === 0);
}
/**
* Check if using a local filesystem.
* @return bool
*/
protected function isLocal()
{
return strtolower(config('filesystems.default')) === 'local';
}
}

View File

@@ -5,9 +5,7 @@ use BookStack\View;
class ViewService
{
protected $view;
protected $user;
protected $permissionService;
/**
@@ -18,7 +16,6 @@ class ViewService
public function __construct(View $view, PermissionService $permissionService)
{
$this->view = $view;
$this->user = auth()->user();
$this->permissionService = $permissionService;
}
@@ -29,8 +26,9 @@ class ViewService
*/
public function add(Entity $entity)
{
if ($this->user === null) return 0;
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
$user = user();
if ($user === null || $user->isDefault()) return 0;
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
$view->increment('views');
@@ -39,7 +37,7 @@ class ViewService
// Otherwise create new view count
$entity->views()->save($this->view->create([
'user_id' => $this->user->id,
'user_id' => $user->id,
'views' => 1
]));
@@ -78,13 +76,14 @@ class ViewService
*/
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
if ($this->user === null) return collect();
$user = user();
if ($user === null || $user->isDefault()) return collect();
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', auth()->user()->id);
$query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');

View File

@@ -1,13 +1,16 @@
<?php namespace BookStack;
use BookStack\Notifications\ResetPassword;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword;
use Authenticatable, CanResetPassword, Notifiable;
/**
* The database table used by the model.
@@ -34,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
protected $permissions;
/**
* Returns a default guest user.
* Returns the default public user.
* @return User
*/
public static function getDefault()
{
return new static([
'email' => 'guest',
'name' => 'Guest'
]);
return static::where('system_name', '=', 'public')->first();
}
/**
* Check if the user is the default public user.
* @return bool
*/
public function isDefault()
{
return $this->system_name === 'public';
}
/**
* The roles that belong to the user.
* @return BelongsToMany
*/
public function roles()
{
if ($this->id === 0) return ;
return $this->belongsToMany(Role::class);
}
@@ -62,6 +74,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->roles->pluck('name')->contains($role);
}
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasSystemRole($role)
{
return $this->roles->pluck('system_name')->contains('admin');
}
/**
* Get all permissions belonging to a the current user.
* @param bool $cache
@@ -138,8 +160,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getAvatar($size = 50)
{
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png');
return baseUrl($this->avatar->getThumb($size, $size, false));
$default = baseUrl('/user_avatar.png');
$imageId = $this->image_id;
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
try {
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
} catch (\Exception $err) {
$avatar = $default;
}
return $avatar;
}
/**
@@ -183,4 +213,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return '';
}
/**
* Send the password reset notification.
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPassword($token));
}
}

View File

@@ -11,29 +11,39 @@ use BookStack\Ownable;
*/
function versioned_asset($file = '')
{
// Don't require css and JS assets for testing
if (config('app.env') === 'testing') return '';
static $version = null;
static $manifest = null;
$manifestPath = 'build/manifest.json';
if (is_null($manifest) && file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
} else if (!file_exists($manifestPath)) {
if (config('app.env') !== 'production') {
$path = public_path($manifestPath);
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
} else {
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
}
throw new \Exception($error);
if (is_null($version)) {
$versionFile = base_path('version');
$version = trim(file_get_contents($versionFile));
}
if (isset($manifest[$file])) {
return baseUrl($manifest[$file]);
$additional = '';
if (config('app.env') === 'development') {
$additional = sha1_file(public_path($file));
}
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
$path = $file . '?version=' . urlencode($version) . $additional;
return baseUrl($path);
}
/**
* Helper method to get the current User.
* Defaults to public 'Guest' user if not logged in.
* @return \BookStack\User
*/
function user()
{
return auth()->user() ?: \BookStack\User::getDefault();
}
/**
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser()
{
return auth()->user() && !auth()->user()->isDefault();
}
/**
@@ -47,7 +57,7 @@ function versioned_asset($file = '')
function userCan($permission, Ownable $ownable = null)
{
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
return user() && user()->can($permission);
}
// Check permission on ownable item
@@ -59,11 +69,12 @@ function userCan($permission, Ownable $ownable = null)
* Helper to access system settings.
* @param $key
* @param bool $default
* @return mixed
* @return bool|string|\BookStack\Services\SettingService
*/
function setting($key, $default = false)
function setting($key = null, $default = false)
{
$settingService = app('BookStack\Services\SettingService');
$settingService = resolve(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
return $settingService->get($key, $default);
}
@@ -79,11 +90,17 @@ function baseUrl($path, $forceAppDomain = false)
if ($isFullUrl && !$forceAppDomain) return $path;
$path = trim($path, '/');
// Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) {
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
// Return normal url path if not specified in config
if (config('app.url') === '') {
return url($path);
}
return rtrim(config('app.url'), '/') . '/' . $path;
}
@@ -109,6 +126,16 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
return app('redirect')->to($to, $status, $headers, $secure);
}
function icon($name, $attrs = []) {
$iconPath = resource_path('assets/icons/' . $name . '.svg');
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
$fileContents = file_get_contents($iconPath);
return str_replace('<svg', '<svg' . $attrString, $fileContents);
}
/**
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
@@ -122,14 +149,14 @@ function sortUrl($path, $data, $overrideData = [])
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} else {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') continue;

View File

@@ -5,23 +5,27 @@
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.2.*",
"php": ">=5.6.4",
"laravel/framework": "5.4.*",
"ext-tidy": "*",
"intervention/image": "^2.3",
"laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.0",
"laravel/socialite": "^3.0",
"barryvdh/laravel-ide-helper": "^2.2.3",
"barryvdh/laravel-debugbar": "^2.3.2",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "0.6.*",
"predis/predis": "^1.0"
"barryvdh/laravel-dompdf": "^0.8",
"predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0",
"socialiteproviders/slack": "^3.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1",
"symfony/dom-crawler": "~3.0",
"symfony/css-selector": "~3.0"
"phpunit/phpunit": "~5.0",
"symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "3.1.*"
},
"autoload": {
"classmap": [
@@ -32,26 +36,26 @@
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize"
],
"pre-update-cmd": [
"php artisan clear-compiled"
],
"post-update-cmd": [
"php artisan optimize"
],
"post-root-package-install": [
"php -r \"copy('.env.example', '.env');\""
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize",
"php artisan cache:clear",
"php artisan view:clear"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
]
},
"config": {

2479
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ return [
|
*/
'locale' => 'en',
'locale' => env('APP_LANG', 'en'),
/*
|--------------------------------------------------------------------------
@@ -138,7 +138,8 @@ return [
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
/**
* Third Party
@@ -147,6 +148,7 @@ return [
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class,
/*
@@ -156,6 +158,7 @@ return [
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\BroadcastServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class,
@@ -194,6 +197,7 @@ return [
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
@@ -215,6 +219,7 @@ return [
'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
/**

View File

@@ -82,7 +82,7 @@ return [
'mysql_testing' => [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),

View File

@@ -1,6 +1,6 @@
<?php
return array(
return [
/*
|--------------------------------------------------------------------------
@@ -13,7 +13,7 @@ return array(
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
'defines' => [
/**
* The location of the DOMPDF font directory
*
@@ -143,7 +143,7 @@ return array(
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
/**
* The default paper size.
@@ -260,7 +260,7 @@ return array(
"DOMPDF_ENABLE_HTML5PARSER" => true,
),
],
);
];

View File

@@ -56,7 +56,7 @@ return [
'local' => [
'driver' => 'local',
'root' => public_path(),
'root' => base_path(),
],
'ftp' => [

View File

@@ -41,12 +41,35 @@ return [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
],
'google' => [
'client_id' => env('GOOGLE_APP_ID', false),
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
],
'slack' => [
'client_id' => env('SLACK_APP_ID', false),
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
],
'facebook' => [
'client_id' => env('FACEBOOK_APP_ID', false),
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
],
'twitter' => [
'client_id' => env('TWITTER_APP_ID', false),
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
],
'ldap' => [
@@ -55,7 +78,8 @@ return [
'pass' => env('LDAP_PASS', false),
'base_dn' => env('LDAP_BASE_DN', false),
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false)
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
]
];

View File

@@ -6,8 +6,12 @@
return [
'app-name' => 'BookStack',
'app-logo' => '',
'app-name-header' => true,
'app-editor' => 'wysiwyg',
'app-color' => '#0288D1',
'app-color-light' => 'rgba(21, 101, 192, 0.15)'
'app-color-light' => 'rgba(21, 101, 192, 0.15)',
'app-custom-head' => false,
'registration-enabled' => false,
];

18
config/snappy.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
return [
'pdf' => [
'enabled' => true,
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
'timeout' => false,
'options' => [],
'env' => [],
],
'image' => [
'enabled' => false,
'binary' => '/usr/local/bin/wkhtmltoimage',
'timeout' => false,
'options' => [],
'env' => [],
],
];

View File

@@ -59,4 +59,14 @@ $factory->define(BookStack\Tag::class, function ($faker) {
'name' => $faker->city,
'value' => $faker->sentence(3)
];
});
$factory->define(BookStack\Image::class, function ($faker) {
return [
'name' => $faker->slug . '.jpg',
'url' => $faker->url,
'path' => $faker->url,
'type' => 'gallery',
'uploaded_to' => 0
];
});

View File

@@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration
// Set all current users as admins
// (At this point only the initially create user should be an admin)
$users = DB::table('users')->get();
$users = DB::table('users')->get()->all();
foreach ($users as $user) {
DB::table('role_user')->insert([
'role_id' => $adminId,

View File

@@ -0,0 +1,66 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemoveHiddenRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Remove the hidden property from roles
Schema::table('roles', function(Blueprint $table) {
$table->dropColumn('hidden');
});
// Add column to mark system users
Schema::table('users', function(Blueprint $table) {
$table->string('system_name')->nullable()->index();
});
// Insert our new public system user.
$publicUserId = DB::table('users')->insertGetId([
'email' => 'guest@example.com',
'name' => 'Guest',
'system_name' => 'public',
'email_confirmed' => true,
'created_at' => \Carbon\Carbon::now(),
'updated_at' => \Carbon\Carbon::now(),
]);
// Get the public role
$publicRole = DB::table('roles')->where('system_name', '=', 'public')->first();
// Connect the new public user to the public role
DB::table('role_user')->insert([
'user_id' => $publicUserId,
'role_id' => $publicRole->id
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('roles', function(Blueprint $table) {
$table->boolean('hidden')->default(false);
$table->index('hidden');
});
DB::table('users')->where('system_name', '=', 'public')->delete();
Schema::table('users', function(Blueprint $table) {
$table->dropColumn('system_name');
});
DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAttachmentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('attachments', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('path');
$table->string('extension', 20);
$table->integer('uploaded_to');
$table->boolean('external');
$table->integer('order');
$table->integer('created_by');
$table->integer('updated_by');
$table->index('uploaded_to');
$table->timestamps();
});
// Get roles with permissions we need to change
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Attachment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('attachments');
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Attachment';
foreach ($ops as $op) {
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCacheTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->unique();
$table->text('value');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cache');
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSessionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->unique();
$table->integer('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sessions');
}
}

View File

@@ -11,14 +11,14 @@ class DummyContentSeeder extends Seeder
*/
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$user = factory(\BookStack\User::class)->create();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
$books = factory(BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
$books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);

View File

@@ -1,27 +1,8 @@
var elixir = require('laravel-elixir');
// Custom extensions
var gulp = require('gulp');
var Task = elixir.Task;
var fs = require('fs');
elixir.extend('queryVersion', function(inputFiles) {
new Task('queryVersion', function() {
var manifestObject = {};
var uidString = Date.now().toString(16).slice(4);
for (var i = 0; i < inputFiles.length; i++) {
var file = inputFiles[i];
manifestObject[file] = file + '?version=' + uidString;
}
var fileContents = JSON.stringify(manifestObject, null, 1);
fs.writeFileSync('public/build/manifest.json', fileContents);
}).watch(['./public/css/*.css', './public/js/*.js']);
});
elixir(function(mix) {
mix.sass('styles.scss')
.sass('print-styles.scss')
.sass('export-styles.scss')
.browserify('global.js', 'public/js/common.js')
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
elixir(mix => {
mix.sass('styles.scss');
mix.sass('print-styles.scss');
mix.sass('export-styles.scss');
mix.browserify('global.js', './public/js/common.js');
});

View File

@@ -1,20 +1,24 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.9.0"
"scripts": {
"build": "gulp --production",
"dev": "gulp watch",
"watch": "gulp watch"
},
"dependencies": {
"devDependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.14.0",
"babel-runtime": "^5.8.29",
"bootstrap-sass": "^3.0.0",
"angular-ui-sortable": "^0.15.0",
"dropzone": "^4.0.1",
"laravel-elixir": "^5.0.0",
"gulp": "^3.9.0",
"laravel-elixir": "^6.0.0-11",
"laravel-elixir-browserify-official": "^0.1.3",
"marked": "^0.3.5",
"moment": "^2.12.0",
"zeroclipboard": "^2.2.0"
"moment": "^2.12.0"
},
"dependencies": {
"clipboard": "^1.5.16"
}
}

View File

@@ -1,5 +0,0 @@
suites:
main:
namespace: BookStack
psr4_prefix: BookStack
src_path: app

View File

@@ -22,6 +22,7 @@
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="APP_LANG" value="en"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
@@ -30,6 +31,7 @@
<env name="AUTH_METHOD" value="standard"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
<env name="LDAP_VERSION" value="3"/>
<env name="STORAGE_TYPE" value="local"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>

Binary file not shown.

View File

@@ -1,5 +0,0 @@
{
"css/styles.css": "css/styles.css?version=ba2edb5",
"css/print-styles.css": "css/print-styles.css?version=ba2edb5",
"js/common.js": "js/common.js?version=ba2edb5"
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
.faded-small,.print-hidden,header{display:none}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
tinymce.PluginManager.add("advlist",function(a){function b(a,b){var c=[];return tinymce.each(b.split(/[ ,]/),function(a){c.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),c}function c(b,c){a.undoManager.transact(function(){var d,e=a.dom,f=a.selection;d=e.getParent(f.getNode(),"ol,ul"),d&&d.nodeName==b&&c!==!1||a.execCommand("UL"==b?"InsertUnorderedList":"InsertOrderedList"),c=c===!1?g[b]:c,g[b]=c,d=e.getParent(f.getNode(),"ol,ul"),d&&(e.setStyle(d,"listStyleType",c?c:null),d.removeAttribute("data-mce-style")),a.focus()})}function d(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var e,f,g={};e=b("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),f=b("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square")),a.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:e,onshow:d,onselect:function(a){c("OL",a.control.settings.data)},onclick:function(){c("OL",!1)}}),a.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:f,onshow:d,onselect:function(a){c("UL",a.control.settings.data)},onclick:function(){c("UL",!1)}})});
tinymce.PluginManager.add("advlist",function(a){function b(a,b){var c=[];return tinymce.each(b.split(/[ ,]/),function(a){c.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),c}function c(b,c){a.undoManager.transact(function(){var d,e=a.dom,f=a.selection;if(d=e.getParent(f.getNode(),"ol,ul"),!d||d.nodeName!=b||c===!1){var h={"list-style-type":c?c:""};a.execCommand("UL"==b?"InsertUnorderedList":"InsertOrderedList",!1,h)}c=c===!1?g[b]:c,g[b]=c,d=e.getParent(f.getNode(),"ol,ul"),d&&(e.setStyle(d,"listStyleType",c?c:null),d.removeAttribute("data-mce-style")),a.focus()})}function d(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var e,f,g={};e=b("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),f=b("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square")),a.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:e,onshow:d,onselect:function(a){c("OL",a.control.settings.data)},onclick:function(){c("OL",!1)}}),a.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:f,onshow:d,onselect:function(a){c("UL",a.control.settings.data)},onclick:function(){c("UL",!1)}})});

File diff suppressed because one or more lines are too long

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