Compare commits

..

131 Commits

Author SHA1 Message Date
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
8933179017 Prevented drafts from showing up in a book sort
Added tests to cover regresssion.
In reference to #100.
2016-04-15 19:51:27 +01:00
Dan Brown
792e786880 Added lists plugin to TinyMCE, Fixed strange nested list behaviour
Fixes #97
2016-04-14 22:34:33 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
0a8030306e Fixed redis config not being set if not using redis 2016-04-12 20:08:49 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
d6bad01130 Fixed draft time display, Cleaned up some code
Cleaned up some comment spacing in book controller and refactored some of the view service functions.
2016-04-09 14:26:42 +01:00
Dan Brown
a33deed26b Added user listing to role edit screen 2016-04-09 13:57:38 +01:00
Dan Brown
2e7345f4f0 Localised draft save time display
In reference to #83.
2016-04-09 13:36:32 +01:00
Dan Brown
1a7de4c2d6 Fixed and cleaned some permisison/role based components 2016-04-09 12:37:58 +01:00
Dan Brown
66ba773367 Updated composer dependancies 2016-04-09 10:56:10 +01:00
Dan Brown
afc3583be8 Made new pages in chapters have a better inital priority 2016-04-07 19:03:00 +01:00
Dan Brown
cbff2c6035 Added uploaded to book/page filters & search in image manager
Also refactored tab styles which affected the settings area.

Closes #41
2016-04-03 14:59:54 +01:00
Dan Brown
8e614ecb6e Updated tests to match recent email confirmation changes 2016-04-03 12:34:10 +01:00
Dan Brown
d099885fd1 Fixed some label casing and fixed incorrect notifications
Fixed the book & chapter permission update notification stating the 'page permissions' have been updated.
2016-04-03 12:19:44 +01:00
Dan Brown
2bb8c3d914 Made email confirmations work with LDAP auth
The email_confirmed user field now actually indicates if an email is confirmed rather than defaulting to true if not checked.
 This ensures toggleing the 'Require email confirmation' setting actually makes all currently unconfirmed users confirm thier emails.
2016-04-03 12:16:54 +01:00
Dan Brown
4caa61fe96 Added a friendlier error for LDAP new user mismatches 2016-04-03 11:16:49 +01:00
Dan Brown
c5960f9b6a Added Redis cache/session support 2016-04-03 11:00:14 +01:00
Dan Brown
412eed19c3 Removed old input checks on entity permission checkboxes
Old input check potentialy causing issues (#89) and is not needed on the pages which it shows.
2016-04-03 10:23:16 +01:00
Dan Brown
e9b596d3bc Merge bugfixes from branch 'v0.8' 2016-03-30 21:49:25 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
8b109bac13 Trimmed long names in header
Fixes #87
2016-03-30 21:28:38 +01:00
Dan Brown
097d9c9f3c Updated entity restrictions to allow permissions, Not just restrict
Also changed wording from 'Restrictions' to 'Permissions' to keep things more familiar and to better reflect what they do.

Referenced in issue #89.
2016-03-30 20:15:44 +01:00
Dan Brown
e7d8a041a8 Merge pull request #84 from ssddanbrown/markdown_editor
Initial implementation of a markdown editor. Closes #57.
2016-03-29 20:18:11 +01:00
Dan Brown
dc2978824e Added basic system tests for markdown editor, Added extra test helpers
Added test helpers for checking if an element exists / does not exist on a page.
Also fixed markdown editor bugs found while creating tests.
2016-03-29 20:13:23 +01:00
Dan Brown
e1994ef2cf Added editor control in admin settings & Fixed some markdown editor bugs
Also updated the setting system with a more sane approach to handling default values. (Now done via the setting-defaults config file)
2016-03-29 19:26:13 +01:00
Dan Brown
efb49019d4 Integrated the markdown editor with the image manager 2016-03-29 18:25:54 +01:00
Dan Brown
ef874712bb Cut readme down and added useful links
Remove a lot of the instructions/config info since much of it is now on the BookStack docs site.
2016-03-25 15:17:04 +00:00
Dan Brown
26965fa08f Added a markdown editor 2016-03-25 14:41:15 +00:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
491f73e0cd Fixed bug causing permission error on save and fixed non-gallery image save 2016-03-13 15:37:46 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
4656c12f6d Fixed bug causing editing notification to always show
Updated tests to prevent happening again
2016-03-13 14:33:43 +00:00
Dan Brown
a06321675a Forced browser scrollbar, Added custom theme compatible logo
Also removed link to search all pages if no pages mached search on global search page.
Closes #74.
2016-03-13 14:00:24 +00:00
Dan Brown
dbe11c1360 Attached images to pages and added restriction filtering
Closes #79
2016-03-13 13:30:47 +00:00
Dan Brown
75ecf1c44d Fixed inset chapter list colors
The colors were being overridden by the custom theme color.
Styles added to force color to page and/or page draft.
2016-03-13 12:43:42 +00:00
Dan Brown
5283919d24 Added new page drafts and started image entity attaching
Closes #80.
2016-03-13 12:04:08 +00:00
Dan Brown
ced8c8e497 Merged branch autosaving_drafts into master 2016-03-12 16:31:37 +00:00
Dan Brown
bf7852ce85 Organised test files & added page update draft tests
Also cleaned styling for new autosave ui parts.
Closes #36.
2016-03-12 16:31:02 +00:00
Dan Brown
30214fde74 Added UI components of page autosaving 2016-03-12 15:52:19 +00:00
Dan Brown
e9c213f803 Fixed firefox list editing bug and made button text size more consistent
Fixes #77
2016-03-09 23:23:28 +00:00
Dan Brown
9f11e045a5 Updated tinyMCE library from 4.3.2 to 4.3.7 2016-03-09 23:20:47 +00:00
Dan Brown
93ebdf724b Changed direct attributes to prevent conflicts 2016-03-09 22:54:18 +00:00
Dan Brown
59ce228c2e Moved page editing to angular controller and started work on update drafts 2016-03-09 22:32:07 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
1d6137f7e2 Added restrictions to user profile lists 2016-03-06 13:17:46 +00:00
Dan Brown
66c56e9d02 Added settings helper and formatted code in some files 2016-03-06 12:55:08 +00:00
Dan Brown
e744d4c82c Changed color picker library and moved color logic to front end
Since the old library was GPLv3 i changed the color picker to tiny-color-picker which is MIT.
Also extracted the styles to a shared view and move color calculation logic to javascript side.
2016-03-06 10:52:10 +00:00
Dan Brown
0774ecc89c Merge branch 'master' into nwalke-update_site_color 2016-03-06 09:08:20 +00:00
Dan Brown
5e7a4c7fb5 Fixed incorrect recents pages on homescreen
Fixed the bug causing the recently updated pages to be exaclty the same as the recently create pages.
Also added in tests to prevent regression.
2016-03-05 22:54:53 +00:00
Dan Brown
76eaf64f94 Fixed errors that occured when merging & refactored entity repositories
Also deleted the git '.orig' files that got added in last merge.
2016-03-05 19:00:26 +00:00
Dan Brown
80865b30a5 Merge branch 'custom_role_system'
Conflicts:
	app/Repos/BookRepo.php
	app/Repos/ChapterRepo.php
	app/Repos/PageRepo.php
2016-03-05 18:21:44 +00:00
Dan Brown
8e6248f57f Added restriction tests and fixed any bugs in the process
Also updated many styles within areas affected by the new permission and roles system.
2016-03-05 18:09:21 +00:00
Dan Brown
268db6b1d0 Added a whole load of permission & role tests 2016-03-05 12:09:09 +00:00
Dan Brown
479dd80a8c Made memcached config allow mulitple servers 2016-03-05 09:47:24 +00:00
Dan Brown
069431db72 Merge branch 'add_memcached' of git://github.com/nwalke/BookStack into nwalke-add_memcached 2016-03-05 09:05:27 +00:00
Dan Brown
bc2b310638 Updated fulltext search with custom escaped query
Changed pdo quoted query to use custom escaping and added like searches when quoted terms are used.
2016-03-04 20:49:35 +00:00
Nick Walke
33bf20cfc8 Found the source of the issue, not sure how to fix 2016-03-04 02:33:57 -06:00
Nick Walke
e3bdc391cd Closes #55. Allows you to set the primary color.
This certainly should not be seen as the final implementation of this.
I imagine that requests like this will happen for many other colors in
the system, it might be good at some point to allow them to upload their
own stylesheet to be used.
2016-03-03 12:01:14 -06:00
Nick Walke
5681f4dd69 Closes #70.
Added the ability to search by multi-word terms using double quotes.
2016-03-02 21:38:23 -06:00
Nick Walke
38d822e04c Closes #69. Implemented and tested memcached. 2016-03-02 20:44:00 -06:00
Dan Brown
8e274a5a84 Refactored some permission controls and increased testing for roles system 2016-03-02 22:35:01 +00:00
Dan Brown
985d2f1c2c Tied entity restriction system into userCan checks 2016-02-29 20:31:21 +00:00
Dan Brown
7f5872372d Added in restriction queries for most lists 2016-02-28 19:03:13 +00:00
Dan Brown
201f788806 Implemented database structure and inital interfaces for entity restrictions 2016-02-28 10:49:41 +00:00
Dan Brown
a14b5c33fd Added missing permission checkboxes and improved image AJAX permission responses 2016-02-27 20:52:46 +00:00
Dan Brown
473261be35 Finished initial implementation of custom role system 2016-02-27 19:24:42 +00:00
Dan Brown
a54be85185 Started work on exposing the role system as editable 2016-02-26 23:44:02 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
54e3122540 Added smarter page finding so changing the page name does not break old urls
Added page & book slug history to revisions so they can be looked up if a page is not found.
2016-02-25 20:01:59 +00:00
Dan Brown
d339ab1125 Updated phpunit testing mail settings due to laravel 5.2 changes 2016-02-22 21:28:53 +00:00
Dan Brown
3ab09ef708 Fixed issue with the book sort not showing all books in sidebar 2016-02-22 21:28:20 +00:00
Dan Brown
c86a122d80 Added table header row styling, made code blocks horizontally scrollbable fixed link popup on firefox 2016-02-22 20:18:08 +00:00
Dan Brown
3a58e37838 Updated phpunit environment variables with some required defaults 2016-02-22 19:39:51 +00:00
Dan Brown
6bd49bcd4b Fixed page listing excerpts not showing due to encoding issues 2016-02-21 13:15:46 +00:00
Dan Brown
61577cf6bf Added entity-specific search results pages. Cleaned & Fixed search results bugs
Added search result pages for pages, chapters and books.
Limited the results on the global search as it just listed out an infinate amount.
Fixed styling on new detailed page listings and also removed the 'bars' from the side to create  a cleaner view.
Fixed bad sql fulltext query format that may have thrown off searches.
Reduced the number of database queries down a thousand or so.
2016-02-21 12:53:58 +00:00
Dan Brown
b4dec2a99c Made page anchor hashes more relevant to the page content
This will help when adding support for new kinds of page content such as markdown as we won't be able to reference the same ID's as before thus they would break on every save.
2016-02-21 11:29:46 +00:00
Dan Brown
fe0b122aca Merge pull request #63 from AkibaWolf/patch-1
Fixes encoding issues the page HTML is formatted on save
2016-02-20 19:24:32 +00:00
Dan Brown
8eb2960950 Added recently created & updated page listings
Closes #46.
2016-02-20 18:51:01 +00:00
AkibaWolf
c2369a740d Update PageRepo.php
Fix encoding problem.

By default DOMDocument::loadHTML treats a string as being encoded with ISO-8859-1. This causes a problem with saving cyrillic pages' text that becomes completely unreadable (like Проверка instead of normal symbols).
2016-02-20 21:31:21 +05:00
Dan Brown
bab6fd1f2f Added recent pages to home view and made the home content more compact 2016-02-20 12:37:06 +00:00
Dan Brown
86fbc9a936 Added tests for profile pages 2016-02-18 19:32:07 +00:00
Dan Brown
4d9726dbdd Added content to user profile pages 2016-02-17 22:11:48 +00:00
Dan Brown
4442a2e6d1 Started work on user profile pages 2016-02-16 21:25:11 +00:00
Dan Brown
293be7093c Merged origin/master into master 2016-02-11 22:36:49 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
354912a1df Made book-navigation sidebar on pages sticky 2016-02-11 22:23:19 +00:00
Nick Walke
eacff3a9f0 Fixes #45 2016-02-11 14:02:17 -06:00
Dan Brown
990acbb9ac Merge pull request #59 from nwalke/master
Updated email on users list to be a link
2016-02-11 18:53:12 +00:00
Nick Walke
17d4533e45 Fixes #58 2016-02-11 01:18:01 -06:00
Dan Brown
d6c00a85ad Fixed incorrect notification when deleting a page 2016-02-10 12:48:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
e0279f93f9 Added a back-to-top button on all pages
The new back-to-top button will show after scrolling a short distance down a long page.
Closes #44.
2016-02-08 20:42:41 +00:00
Dan Brown
9b83c57316 Fixed some design issues and improved page export styling
Fixed alignment on export options dropdown.
Fixed bullet list items sitting too close next to floated content. Fixes #34.
Fixed text overlaying images in PDF exports (Floats removed for now). Fixes #53.
Fixed spaced table cells on html & PDF exports.
2016-02-08 20:41:40 +00:00
Dan Brown
5d73d17c74 Fixed bug preventing LDAP users updating thier profile
Made email not required when a profile is updated. LDAP users without admin privileges could did not have an email field to submit therefore could previously not update thier profile.
2016-02-08 20:35:23 +00:00
Dan Brown
d32460070f Made ldap auth use the 'dn' if a 'uid' is not present.
Fixes #56
2016-02-08 19:45:01 +00:00
Dan Brown
105500e506 Tweaked page form header and added public uploads folder into repo 2016-02-07 10:21:09 +00:00
Dan Brown
8296782149 Updated image controller styling and added preview option
The notification system was also updated so it can be used from JavaScript events such as image manager uploads.

Closes #25
2016-02-07 10:17:38 +00:00
Dan Brown
8e8d582bc6 Updated app requirements & Added some friendlier errors 2016-02-03 20:55:37 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
e87db96fc0 Updated folder permissions installation instructions & fixed issue with handling image saving on user creation 2016-02-02 07:34:48 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
4bb7f0613f Fixed issue with initial user not having a password 2016-02-01 18:30:50 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
080acf0a62 Added plaintext & basic PDF page Export 2016-01-31 17:53:30 +00:00
Dan Brown
ea2e16cabb Added page HTML export 2016-01-20 22:13:13 +00:00
Dan Brown
7bcd967fd9 Increased LDAP testing and fixed any Auth-based bugs found 2016-01-17 15:20:07 +00:00
Dan Brown
bb87401d10 Improved image upload error handling and fixed bad user-avatar rewriting 2016-01-17 15:19:26 +00:00
Dan Brown
0821672e70 Cleaned tests up, Started LDAP tests, Created LDAP wrapper 2016-01-15 23:21:47 +00:00
Dan Brown
14feef3679 Updated user interfaces for LDAP and added email from LDAP 2016-01-13 22:22:30 +00:00
Dan Brown
1c8c9e65c5 Got LDAP auth working to a functional state 2016-01-11 22:41:05 +00:00
Dan Brown
14ca31768c Updated laravel to 5.2 and started ldap implementation 2016-01-09 19:24:21 +00:00
Dan Brown
e27a630a09 Updated readme with social & update instructions 2016-01-02 16:24:09 +00:00
Dan Brown
9319f99a3d Updated readme with installation clarification 2016-01-02 15:02:19 +00:00
Dan Brown
d6739c1158 Merge branch 'master' of github.com:ssddanbrown/BookStack 2016-01-02 14:51:36 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
d3709de035 Added more tests to increase test coverage 2016-01-02 14:48:35 +00:00
Dan Brown
7178c66cf5 Fix error with database migrate command 2016-01-01 21:46:55 +00:00
Dan Brown
f60a0c3b76 Improved 404 page and updated tests for empty search 2016-01-01 09:03:40 +00:00
Dan Brown
9a470b07fd Added public build folder and support for a demo mode 2015-12-31 17:57:34 +00:00
Dan Brown
0d8ca22487 Swapped vue for angular js 2015-12-31 17:32:03 +00:00
244 changed files with 8816 additions and 1946 deletions

View File

@@ -7,13 +7,22 @@ APP_KEY=SomeRandomString
DB_HOST=localhost
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database__user_password
DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
@@ -25,6 +34,9 @@ STORAGE_S3_BUCKET=false
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
@@ -33,8 +45,16 @@ GOOGLE_APP_SECRET=false
# URL used for social login redirects, NO TRAILING SLASH
APP_URL=http://bookstack.dev
# External services
USE_GRAVATAR=true
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Mail settings
MAIL_DRIVER=smtp

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ Homestead.yaml
/public/plugins
/public/css/*.map
/public/js/*.map
/public/uploads
/public/bower
/storage/images
_ide_helper.php

View File

@@ -15,15 +15,11 @@ class Activity extends Model
/**
* Get the entity for this activity.
* @return bool
*/
public function entity()
{
if ($this->entity_id) {
return $this->morphTo('entity')->first();
} else {
return false;
}
if ($this->entity_type === '') $this->entity_type = null;
return $this->morphTo('entity');
}
/**

View File

@@ -1,14 +1,9 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
abstract class Entity extends Model
abstract class Entity extends Ownable
{
use Ownable;
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
@@ -31,11 +26,7 @@ abstract class Entity extends Model
if ($matches) return true;
if ($entity->isA('chapter') && $this->isA('book')) {
return $entity->book_id === $this->id;
}
if ($entity->isA('page') && $this->isA('book')) {
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
return $entity->book_id === $this->id;
}
@@ -57,7 +48,6 @@ abstract class Entity extends Model
/**
* Get View objects for this entity.
* @return mixed
*/
public function views()
{
@@ -65,12 +55,22 @@ abstract class Entity extends Model
}
/**
* Get just the views for the current user.
* @return mixed
* Get this entities restrictions.
*/
public function userViews()
public function restrictions()
{
return $this->views()->where('user_id', '=', auth()->user()->id);
return $this->morphMany('BookStack\Restriction', 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
{
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
}
/**
@@ -85,23 +85,14 @@ abstract class Entity extends Model
}
/**
* Gets the class name.
* @return string
*/
public static function getClassName()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
/**
*Gets a limited-length version of the entities name.
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public function getShortName($length = 25)
{
if(strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length-3) . '...';
if (strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length - 3) . '...';
}
/**
@@ -111,27 +102,48 @@ abstract class Entity extends Model
* @param string[] array $wheres
* @return mixed
*/
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{
$termString = '';
foreach ($terms as $term) {
$termString .= htmlentities($term) . '* ';
$exactTerms = [];
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/\s/', $term)) {
$exactTerms[] = '%' . $term . '%';
$term = '"' . $term . '"';
} else {
$term = '' . $term . '*';
}
if ($term !== '*') $terms[$key] = $term;
}
$termString = implode(' ', $terms);
$fields = implode(',', $fieldsToSearch);
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]);
$search = static::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, $fieldsToSearch) {
foreach ($exactTerms as $exactTerm) {
foreach ($fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if (!static::isA('book')) $search = $search->with('book');
if (static::isA('page')) $search = $search->with('chapter');
if (static::isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if (static::isA('chapter')) {
$search = $search->with('book');
}
return $search->orderBy('title_relevance', 'desc')->get();
return $search->orderBy('title_relevance', 'desc');
}
/**

View File

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

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
}
class ConfirmationEmailException extends NotifyException {}

View File

@@ -3,8 +3,12 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\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;
class Handler extends ExceptionHandler
{
@@ -14,7 +18,10 @@ class Handler extends ExceptionHandler
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
/**
@@ -32,17 +39,27 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof NotifyException) {
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($e instanceof NotifyException) {
\Session::flash('error', $e->message);
return response()->redirectTo($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
}
return parent::render($request, $e);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{
parent::__construct($message, 404);
}
}

View File

@@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PermissionsException extends Exception {}

View File

@@ -0,0 +1,5 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PrettyException extends Exception {}

View File

@@ -1,6 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends \Exception
{
}
class SocialDriverNotConfigured extends PrettyException {}

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
}
class SocialSignInException extends NotifyException {}

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
}
class UserRegistrationException extends NotifyException {}

View File

@@ -2,6 +2,9 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Exceptions\PrettyException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
@@ -29,9 +32,10 @@ class AuthController extends Controller
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $loginPath = '/login';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $username = 'email';
protected $socialAuthService;
protected $emailConfirmationService;
@@ -39,9 +43,9 @@ class AuthController extends Controller
/**
* Create a new authentication controller instance.
* @param SocialAuthService $socialAuthService
* @param SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
@@ -49,6 +53,7 @@ class AuthController extends Controller
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
parent::__construct();
}
@@ -60,15 +65,15 @@ class AuthController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
}
protected function checkRegistrationAllowed()
{
if (!\Setting::get('registration-enabled')) {
if (!setting('registration-enabled')) {
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
}
}
@@ -105,6 +110,46 @@ 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
*/
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('A user with the email ' . $user->email . ' already exists but with different credentials.');
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
return redirect()->intended($this->redirectPath());
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
@@ -118,8 +163,8 @@ class AuthController extends Controller
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
@@ -127,7 +172,7 @@ class AuthController extends Controller
/**
* The registrations flow for all users.
* @param array $userData
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
@@ -135,8 +180,8 @@ class AuthController extends Controller
*/
protected function registerUser(array $userData, $socialAccount = false)
{
if (\Setting::get('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
if (setting('registration-restrict')) {
$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');
@@ -148,21 +193,19 @@ class AuthController extends Controller
$newUser->socialAccounts()->save($socialAccount);
}
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
$newUser->email_confirmed = false;
if (setting('registration-confirmation') || setting('registration-restrict')) {
$newUser->save();
$this->emailConfirmationService->sendConfirmation($newUser);
return redirect('/register/confirm');
}
$newUser->email_confirmed = true;
auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
return redirect($this->redirectPath());
}
/**
* Show the page to tell the user to check thier email
* Show the page to tell the user to check their email
* and confirm their address.
*/
public function getRegisterConfirmation()
@@ -222,7 +265,7 @@ class AuthController extends Controller
]);
$user = $this->userRepo->getByEmail($request->get('email'));
$this->emailConfirmationService->sendConfirmation($user);
\Session::flash('success', 'Confirmation email resent, Please check your inbox.');
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
return redirect('/register/confirm');
}
@@ -232,13 +275,9 @@ class AuthController extends Controller
*/
public function getLogin()
{
if (view()->exists('auth.authenticate')) {
return view('auth.authenticate');
}
$socialDrivers = $this->socialAuthService->getActiveDrivers();
return view('auth.login', ['socialDrivers' => $socialDrivers]);
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
@@ -253,7 +292,7 @@ class AuthController extends Controller
}
/**
* Redirect to the social site for authentication initended to register.
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
*/

View File

@@ -1,12 +1,9 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
@@ -19,24 +16,26 @@ class BookController extends Controller
protected $bookRepo;
protected $pageRepo;
protected $chapterRepo;
protected $userRepo;
/**
* BookController constructor.
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Display a listing of the book.
*
* @return Response
*/
public function index()
@@ -50,12 +49,11 @@ class BookController extends Controller
/**
* Show the form for creating a new book.
*
* @return Response
*/
public function create()
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->setPageTitle('Create New Book');
return view('books/create');
}
@@ -68,9 +66,9 @@ class BookController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->bookRepo->newFromInput($request->all());
@@ -84,7 +82,6 @@ class BookController extends Controller
/**
* Display the specified book.
*
* @param $slug
* @return Response
*/
@@ -99,31 +96,29 @@ class BookController extends Controller
/**
* Show the form for editing the specified book.
*
* @param $slug
* @return Response
*/
public function edit($slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle('Edit Book ' . $book->getShortName());
return view('books/edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
*
* @param Request $request
* @param $slug
* @return Response
*/
public function update(Request $request, $slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book->fill($request->all());
@@ -141,8 +136,8 @@ class BookController extends Controller
*/
public function showDelete($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle('Delete Book ' . $book->getShortName());
return view('books/delete', ['book' => $book, 'current' => $book]);
}
@@ -154,10 +149,10 @@ class BookController extends Controller
*/
public function sort($bookSlug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = $this->bookRepo->getChildren($book);
$books = $this->bookRepo->getAll();
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book, true);
$books = $this->bookRepo->getAll(false);
$this->setPageTitle('Sort Book ' . $book->getShortName());
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
@@ -177,15 +172,14 @@ class BookController extends Controller
/**
* Saves an array of sort mapping to pages and chapters.
*
* @param string $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function saveSort($bookSlug, Request $request)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->has('sort-tree')) {
@@ -223,17 +217,48 @@ class BookController extends Controller
/**
* Remove the specified book from storage.
*
* @param $bookSlug
* @return Response
*/
public function destroy($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book);
$this->bookRepo->destroyBySlug($bookSlug);
return redirect('/books');
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
'book' => $book,
'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
session()->flash('success', 'Book Restrictions Updated');
return redirect($book->getUrl());
}
}

View File

@@ -1,13 +1,9 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use Views;
@@ -17,20 +13,22 @@ class ChapterController extends Controller
protected $bookRepo;
protected $chapterRepo;
protected $userRepo;
/**
* ChapterController constructor.
* @param $bookRepo
* @param $chapterRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new chapter.
* @param $bookSlug
@@ -38,8 +36,8 @@ class ChapterController extends Controller
*/
public function create($bookSlug)
{
$this->checkPermission('chapter-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle('Create New Chapter');
return view('chapters/create', ['book' => $book, 'current' => $book]);
}
@@ -52,12 +50,13 @@ class ChapterController extends Controller
*/
public function store($bookSlug, Request $request)
{
$this->checkPermission('chapter-create');
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->newFromInput($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
$chapter->priority = $this->bookRepo->getNewPriority($book);
@@ -81,7 +80,14 @@ class ChapterController extends Controller
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
$pages = $this->chapterRepo->getChildren($chapter);
return view('chapters/show', [
'book' => $book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
]);
}
/**
@@ -92,9 +98,9 @@ class ChapterController extends Controller
*/
public function edit($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
}
@@ -108,9 +114,9 @@ class ChapterController extends Controller
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$chapter->fill($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
$chapter->updated_by = auth()->user()->id;
@@ -127,9 +133,9 @@ class ChapterController extends Controller
*/
public function showDelete($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
}
@@ -142,11 +148,46 @@ class ChapterController extends Controller
*/
public function destroy($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter);
return redirect($book->getUrl());
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
'chapter' => $chapter,
'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
session()->flash('success', 'Chapter Restrictions Updated');
return redirect($chapter->getUrl());
}
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use HttpRequestException;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
@@ -42,6 +43,15 @@ abstract class Controller extends BaseController
$this->signedIn = auth()->check();
}
/**
* Stops the application and shows a permission error if
* the application is in demo mode.
*/
protected function preventAccessForDemoUsers()
{
if (config('app.env') === 'demo') $this->showPermissionError();
}
/**
* Adds the page title into the view.
* @param $title
@@ -51,24 +61,48 @@ abstract class Controller extends BaseController
view()->share('pageTitle', $title);
}
/**
* On a permission error redirect to home and display.
* the error as a notification.
*/
protected function showPermissionError()
{
Session::flash('error', trans('errors.permission'));
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
throw new HttpResponseException($response);
}
/**
* Checks for a permission.
*
* @param $permissionName
* @param string $permissionName
* @return bool|\Illuminate\Http\RedirectResponse
*/
protected function checkPermission($permissionName)
{
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
Session::flash('error', trans('errors.permission'));
throw new HttpResponseException(
redirect('/')
);
$this->showPermissionError();
}
return true;
}
/**
* Check the current user's permissions against an ownable item.
* @param $permission
* @param Ownable $ownable
* @return bool
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
return $this->showPermissionError();
}
/**
* Check if a user has a permission or bypass if the callback is true.
* @param $permissionName
* @param $callback
* @return bool
*/
protected function checkPermissionOr($permissionName, $callback)
{
$callbackResult = $callback();

View File

@@ -3,39 +3,44 @@
namespace BookStack\Http\Controllers;
use Activity;
use Illuminate\Http\Request;
use BookStack\Repos\EntityRepo;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use Views;
class HomeController extends Controller
{
protected $activityService;
protected $bookRepo;
protected $entityRepo;
/**
* HomeController constructor.
* @param BookRepo $bookRepo
* @param EntityRepo $entityRepo
*/
public function __construct(BookRepo $bookRepo)
public function __construct(EntityRepo $entityRepo)
{
$this->bookRepo = $bookRepo;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Display the homepage.
*
* @return Response
*/
public function index()
{
$activity = Activity::latest();
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
return view('home', ['activity' => $activity, 'recents' => $recents]);
$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);
return view('home', [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages
]);
}
}

View File

@@ -1,13 +1,9 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Intervention\Image\Facades\Image as ImageTool;
use Illuminate\Support\Facades\DB;
use BookStack\Image;
use BookStack\Repos\PageRepo;
@@ -19,8 +15,8 @@ class ImageController extends Controller
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
@@ -31,9 +27,9 @@ class ImageController extends Controller
parent::__construct();
}
/**
* Get all images for a specific type, Paginated
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
@@ -43,6 +39,24 @@ class ImageController extends Controller
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType($type, $page = 0, Request $request)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
return response()->json($imgData);
}
/**
* Get all images for a user.
* @param int $page
@@ -54,22 +68,49 @@ class ImageController extends Controller
return response()->json($imgData);
}
/**
* Get gallery images with a specific filter such as book or page
* @param $filter
* @param int $page
* @param Request $request
*/
public function getGalleryFiltered($filter, $page = 0, Request $request)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create');
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'image|mimes:jpeg,gif,png'
]);
$imageUpload = $request->file('file');
$image = $this->imageRepo->saveNew($imageUpload, $type);
try {
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
@@ -83,7 +124,7 @@ class ImageController extends Controller
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create');
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
@@ -91,33 +132,32 @@ class ImageController extends Controller
/**
* Update image details
* @param $imageId
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function update($imageId, Request $request)
{
$this->checkPermission('image-update');
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo
* @param Request $request
* @param int $id
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(PageRepo $pageRepo, Request $request, $id)
{
$this->checkPermission('image-delete');
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);

View File

@@ -1,15 +1,16 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Views;
class PageController extends Controller
@@ -18,24 +19,29 @@ class PageController extends Controller
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $exportService;
protected $userRepo;
/**
* PageController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ExportService $exportService
* @param UserRepo $userRepo
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new page.
*
* @param $bookSlug
* @param bool $chapterSlug
* @return Response
@@ -43,33 +49,60 @@ class PageController extends Controller
*/
public function create($bookSlug, $chapterSlug = false)
{
$this->checkPermission('page-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$this->setPageTitle('Create New Page');
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
$draft = $this->pageRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl());
}
/**
* Store a newly created page in storage.
*
* Show form to continue editing a draft page.
* @param $bookSlug
* @param $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', $draft);
$this->setPageTitle('Edit Page Draft');
return view('pages/create', ['draft' => $draft, 'book' => $book]);
}
/**
* Store a new page by changing a draft into a page.
* @param Request $request
* @param $bookSlug
* @param string $bookSlug
* @return Response
*/
public function store(Request $request, $bookSlug)
public function store(Request $request, $bookSlug, $pageId)
{
$this->checkPermission('page-create');
$this->validate($request, [
'name' => 'required|string|max:255'
'name' => 'required|string|max:255'
]);
$input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug);
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
$input['priority'] = $this->bookRepo->getNewPriority($book);
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
$draftPage = $this->pageRepo->getById($pageId, true);
$chapterId = $draftPage->chapter_id;
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
} else {
$input['priority'] = $this->bookRepo->getNewPriority($parent);
}
$page = $this->pageRepo->publishDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
@@ -77,7 +110,8 @@ 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
* @return Response
@@ -85,32 +119,69 @@ class PageController extends Controller
public function show($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
} catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());
}
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
}
/**
* Get page from an ajax request.
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function getPageAjax($pageId)
{
$page = $this->pageRepo->getById($pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
*
* @param $bookSlug
* @param $pageSlug
* @return Response
*/
public function edit($bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Editing Page ' . $page->getShortName());
$page->isDraft = false;
// Check for active editing
$warnings = [];
if ($this->pageRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->pageRepo->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);
$page->name = $draft->name;
$page->html = $draft->html;
$page->markdown = $draft->markdown;
$page->isDraft = true;
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
}
/**
* Update the specified page in storage.
*
* @param Request $request
* @param $bookSlug
* @param $pageSlug
@@ -118,17 +189,42 @@ class PageController extends Controller
*/
public function update(Request $request, $bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$this->validate($request, [
'name' => 'required|string|max:255'
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->updatePage($page, $book->id, $request->all());
Activity::add($page, 'page_update', $book->id);
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
* @param Request $request
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function saveDraft(Request $request, $pageId)
{
$page = $this->pageRepo->getById($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']));
}
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => 'Draft saved at ',
'timestamp' => $utcUpdateTimestamp
]);
}
/**
* Redirect from a special link url which
* uses the page id rather than the name.
@@ -149,16 +245,32 @@ class PageController extends Controller
*/
public function showDelete($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle('Delete Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
}
/**
* Show the deletion page for the specified page.
* @param $bookSlug
* @param $pageId
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showDeleteDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
}
/**
* Remove the specified page from storage.
*
* @param $bookSlug
* @param $pageSlug
* @return Response
@@ -166,10 +278,28 @@ class PageController extends Controller
*/
public function destroy($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', 'Page deleted');
$this->pageRepo->destroy($page);
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
* @param $bookSlug
* @param $pageId
* @return Response
* @throws NotFoundException
*/
public function destroyDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
session()->flash('success', 'Draft deleted');
$this->pageRepo->destroy($page);
return redirect($book->getUrl());
}
@@ -214,11 +344,125 @@ class PageController extends Controller
*/
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl());
}
/**
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
* https://github.com/barryvdh/laravel-dompdf
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
}
/**
* Export a page to a self-contained HTML file.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
}
/**
* Export a page to a simple plaintext .txt file.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
return view('pages/detailed-listing', [
'title' => 'Recently Created Pages',
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
return view('pages/detailed-listing', [
'title' => 'Recently Updated Pages',
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $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);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Set the restrictions for this page.
* @param $bookSlug
* @param $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);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
session()->flash('success', 'Page Restrictions Updated');
return redirect($page->getUrl());
}
}

View File

@@ -0,0 +1,129 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class PermissionController extends Controller
{
protected $permissionsRepo;
/**
* PermissionController constructor.
* @param PermissionsRepo $permissionsRepo
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
parent::__construct();
}
/**
* Show a listing of the roles in the system.
*/
public function listRoles()
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
return view('settings/roles/index', ['roles' => $roles]);
}
/**
* Show the form to create a new role
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function createRole()
{
$this->checkPermission('user-roles-manage');
return view('settings/roles/create');
}
/**
* Store a new role in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function storeRole(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
]);
$this->permissionsRepo->saveNewRole($request->all());
session()->flash('success', 'Role successfully created');
return redirect('/settings/roles');
}
/**
* Show the form for editing a user role.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function editRole($id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
return view('settings/roles/edit', ['role' => $role]);
}
/**
* Updates a user role.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function updateRole($id, Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
]);
$this->permissionsRepo->updateRole($id, $request->all());
session()->flash('success', 'Role successfully updated');
return redirect('/settings/roles');
}
/**
* Show the view to delete a role.
* Offers the chance to migrate users.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showDeleteRole($id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
}
/**
* Delete a role from the system,
* Migrate from a previous role if set.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function deleteRole($id, Request $request)
{
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
session()->flash('error', $e->getMessage());
return redirect()->back();
}
session()->flash('success', 'Role successfully deleted');
return redirect('/settings/roles');
}
}

View File

@@ -42,11 +42,77 @@ class SearchController extends Controller
return redirect()->back();
}
$searchTerm = $request->get('term');
$pages = $this->pageRepo->getBySearch($searchTerm);
$books = $this->bookRepo->getBySearch($searchTerm);
$chapters = $this->chapterRepo->getBySearch($searchTerm);
$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);
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Page Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $pages,
'title' => 'Page Search Results',
'searchTerm' => $searchTerm
]);
}
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Chapter Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => 'Chapter Search Results',
'searchTerm' => $searchTerm
]);
}
/**
* Search only the books in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchBooks(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
$this->setPageTitle('Book Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $books,
'title' => 'Book Search Results',
'searchTerm' => $searchTerm
]);
}
/**
@@ -62,9 +128,9 @@ class SearchController extends Controller
return redirect()->back();
}
$searchTerm = $request->get('term');
$whereTerm = [['book_id', '=', $bookId]];
$pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm);
$chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm);
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
}

View File

@@ -1,43 +1,40 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use Setting;
class SettingController extends Controller
{
/**
* Display a listing of the settings.
*
* @return Response
*/
public function index()
{
$this->checkPermission('settings-update');
$this->checkPermission('settings-manage');
$this->setPageTitle('Settings');
return view('settings/index');
}
/**
* Update the specified settings in storage.
*
* @param Request $request
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
$this->checkPermission('settings-update');
$this->preventAccessForDemoUsers();
$this->checkPermission('settings-manage');
// Cycles through posted settings and update them
foreach($request->all() as $name => $value) {
if(strpos($name, 'setting-') !== 0) continue;
foreach ($request->all() as $name => $value) {
if (strpos($name, 'setting-') !== 0) continue;
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
}
session()->flash('success', 'Settings Saved');
return redirect('/settings');
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Activity;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -34,7 +35,8 @@ class UserController extends Controller
*/
public function index()
{
$users = $this->user->all();
$this->checkPermission('users-manage');
$users = $this->userRepo->getAllUsers();
$this->setPageTitle('Users');
return view('users/index', ['users' => $users]);
}
@@ -45,8 +47,9 @@ class UserController extends Controller
*/
public function create()
{
$this->checkPermission('user-create');
return view('users/create');
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
return view('users/create', ['authMethod' => $authMethod]);
}
/**
@@ -56,32 +59,47 @@ class UserController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('user-create');
$this->validate($request, [
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:5',
'password-confirm' => 'required|same:password',
'role' => 'required|exists:roles,id'
]);
'email' => 'required|email|unique:users,email'
];
$authMethod = config('auth.method');
if ($authMethod === 'standard') {
$validationRules['password'] = 'required|min:5';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
$validationRules['external_auth_id'] = 'required';
}
$this->validate($request, $validationRules);
$user = $this->user->fill($request->all());
$user->password = bcrypt($request->get('password'));
if ($authMethod === 'standard') {
$user->password = bcrypt($request->get('password'));
} elseif ($authMethod === 'ldap') {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
$user->attachRoleId($request->get('role'));
if ($request->has('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Get avatar from gravatar and save
if (!env('DISABLE_EXTERNAL_SERVICES', false)) {
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
}
return redirect('/users');
return redirect('/settings/users');
}
/**
* Show the form for editing the specified user.
* @param int $id
@@ -90,14 +108,16 @@ class UserController extends Controller
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOr('user-update', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$authMethod = config('auth.method');
$user = $this->user->findOrFail($id);
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle('User Profile');
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]);
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
}
/**
@@ -108,30 +128,45 @@ class UserController extends Controller
*/
public function update(Request $request, $id)
{
$this->checkPermissionOr('user-update', function () use ($id) {
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $id,
'password' => 'min:5',
'password-confirm' => 'same:password',
'role' => 'exists:roles,id'
'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'
]);
$user = $this->user->findOrFail($id);
$user->fill($request->except('password'));
$user->fill($request->all());
if ($this->currentUser->can('user-update') && $request->has('role')) {
$user->attachRoleId($request->get('role'));
// Role updates
if (userCan('users-manage') && $request->has('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Password updates
if ($request->has('password') && $request->get('password') != '') {
$password = $request->get('password');
$user->password = bcrypt($password);
}
// External auth id updates
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
return redirect('/users');
session()->flash('success', 'User successfully updated');
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
return redirect($redirectUrl);
}
/**
@@ -141,9 +176,10 @@ class UserController extends Controller
*/
public function delete($id)
{
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->user->findOrFail($id);
$this->setPageTitle('Delete User ' . $user->name);
return view('users/delete', ['user' => $user]);
@@ -156,7 +192,8 @@ class UserController extends Controller
*/
public function destroy($id)
{
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
@@ -167,6 +204,25 @@ class UserController extends Controller
}
$this->userRepo->destroy($user);
return redirect('/users');
return redirect('/settings/users');
}
/**
* Show the user profile page
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
'assetCounts' => $assetCounts
]);
}
}

View File

@@ -11,14 +11,12 @@ class Authenticate
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
*/
public function __construct(Guard $auth)
@@ -28,17 +26,17 @@ class Authenticate
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if(auth()->check() && auth()->user()->email_confirmed == false) {
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect()->guest('/register/confirm/awaiting');
}
if ($this->auth->guest() && !Setting::get('app-public')) {
if ($this->auth->guest() && !setting('app-public')) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {

View File

@@ -3,6 +3,11 @@
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {
Route::group(['prefix' => 'pages'], function() {
Route::get('/recently-created', 'PageController@showRecentlyCreated');
Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
});
Route::group(['prefix' => 'books'], function () {
// Books
@@ -14,21 +19,30 @@ Route::group(['middleware' => 'auth'], function () {
Route::delete('/{id}', 'BookController@destroy');
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
Route::get('/{slug}', 'BookController@show');
Route::get('/{bookSlug}/permissions', 'BookController@showRestrict');
Route::put('/{bookSlug}/permissions', 'BookController@restrict');
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
Route::post('/{bookSlug}/page/{pageId}', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
//Revisions
// Revisions
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
@@ -40,20 +54,15 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
});
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::get('/users/{id}/delete', 'UserController@delete');
Route::post('/users/create', 'UserController@store');
Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy');
// User Profile routes
Route::get('/user/{userId}', 'UserController@showProfilePage');
// Image routes
Route::group(['prefix' => 'images'], function() {
@@ -66,14 +75,24 @@ Route::group(['middleware' => 'auth'], function () {
Route::post('/{type}/upload', 'ImageController@uploadByType');
Route::get('/{type}/all', 'ImageController@getAllByType');
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
Route::delete('/{imageId}', 'ImageController@destroy');
});
// Ajax routes
Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
// Links
Route::get('/link/{id}', 'PageController@redirectFromLink');
// Search
Route::get('/search/all', 'SearchController@searchAll');
Route::get('/search/pages', 'SearchController@searchPages');
Route::get('/search/books', 'SearchController@searchBooks');
Route::get('/search/chapters', 'SearchController@searchChapters');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
// Other Pages
@@ -81,8 +100,28 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/home', 'HomeController@index');
// Settings
Route::get('/settings', 'SettingController@index');
Route::post('/settings', 'SettingController@update');
Route::group(['prefix' => 'settings'], function() {
Route::get('/', 'SettingController@index');
Route::post('/', 'SettingController@update');
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::get('/users/{id}/delete', 'UserController@delete');
Route::post('/users/create', 'UserController@store');
Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy');
// Roles
Route::get('/roles', 'PermissionController@listRoles');
Route::get('/roles/new', 'PermissionController@createRole');
Route::post('/roles/new', 'PermissionController@storeRole');
Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
Route::get('/roles/{id}', 'PermissionController@editRole');
Route::put('/roles/{id}', 'PermissionController@updateRole');
});
});

View File

@@ -1,14 +1,9 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
use Images;
class Image extends Model
class Image extends Ownable
{
use Ownable;
protected $fillable = ['name'];

View File

@@ -1,7 +1,8 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Model;
trait Ownable
abstract class Ownable extends Model
{
/**
* Relation for the user that created this entity.
@@ -20,4 +21,14 @@ trait Ownable
{
return $this->belongsTo('BookStack\User', 'updated_by');
}
/**
* Gets the class name.
* @return string
*/
public static function getClassName()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
}

View File

@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class Page extends Entity
{
protected $fillable = ['name', 'html', 'priority'];
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
@@ -34,18 +34,21 @@ class Page extends Entity
public function revisions()
{
return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc');
return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc');
}
public function getUrl()
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
return '/books/' . $bookSlug . '/page/' . $this->slug;
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : $this->slug;
return '/books/' . $bookSlug . $midText . $idComponent;
}
public function getExcerpt($length = 100)
{
return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
return mb_convert_encoding($text, 'UTF-8');
}
}

View File

@@ -1,23 +1,33 @@
<?php
namespace BookStack;
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text'];
protected $fillable = ['name', 'html', 'text', 'markdown'];
/**
* Get the user that created the page revision
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
{
return $this->belongsTo('BookStack\User', 'created_by');
}
/**
* Get the page this revision originates from.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
{
return $this->belongsTo('BookStack\Page');
}
/**
* Get the url for this revision.
* @return string
*/
public function getUrl()
{
return $this->page->getUrl() . '/revisions/' . $this->id;

View File

@@ -11,6 +11,16 @@ class Permission extends Model
*/
public function roles()
{
return $this->belongsToMany('BookStack\Permissions');
return $this->belongsToMany('BookStack\Role');
}
/**
* Get the permission object by name.
* @param $roleName
* @return mixed
*/
public static function getByName($name)
{
return static::where('name', '=', $name)->first();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace BookStack\Providers;
use Auth;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
Auth::provider('ldap', function($app, array $config) {
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
});
}
}

View File

@@ -28,11 +28,17 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function() {
return new ActivityService($this->app->make('BookStack\Activity'));
return new ActivityService(
$this->app->make('BookStack\Activity'),
$this->app->make('BookStack\Services\RestrictionService')
);
});
$this->app->bind('views', function() {
return new ViewService($this->app->make('BookStack\View'));
return new ViewService(
$this->app->make('BookStack\View'),
$this->app->make('BookStack\Services\RestrictionService')
);
});
$this->app->bind('setting', function() {
@@ -41,6 +47,7 @@ class CustomFacadeProvider extends ServiceProvider
$this->app->make('Illuminate\Contracts\Cache\Repository')
);
});
$this->app->bind('images', function() {
return new ImageService(
$this->app->make('Intervention\Image\ImageManager'),

View File

@@ -0,0 +1,133 @@
<?php
namespace BookStack\Providers;
use BookStack\Role;
use BookStack\Services\LdapService;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class LdapUserProvider implements UserProvider
{
/**
* The user model.
*
* @var string
*/
protected $model;
/**
* @var LdapService
*/
protected $ldapService;
/**
* LdapUserProvider constructor.
* @param $model
* @param LdapService $ldapService
*/
public function __construct($model, LdapService $ldapService)
{
$this->model = $model;
$this->ldapService = $ldapService;
}
/**
* Create a new instance of the model.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function createModel()
{
$class = '\\' . ltrim($this->model, '\\');
return new $class;
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->where($model->getRememberTokenName(), $token)
->first();
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
if ($user->exists) {
$user->setRememberToken($token);
$user->save();
}
}
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get user via LDAP
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
if ($userDetails === null) return null;
// Search current user base by looking up a uid
$model = $this->createModel();
$currentUser = $model->newQuery()
->where('external_auth_id', $userDetails['uid'])
->first();
if ($currentUser !== null) return $currentUser;
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];
$model->email = $userDetails['email'];
$model->email_confirmed = false;
return $model;
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
}
}

View File

@@ -1,28 +1,35 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
class BookRepo
class BookRepo extends EntityRepo
{
protected $book;
protected $pageRepo;
protected $chapterRepo;
/**
* BookRepo constructor.
* @param Book $book
* @param PageRepo $pageRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
*/
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
{
$this->book = $book;
$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->restrictionService->enforceBookRestrictions($this->book, 'view');
}
/**
@@ -32,7 +39,7 @@ class BookRepo
*/
public function getById($id)
{
return $this->book->findOrFail($id);
return $this->bookQuery()->findOrFail($id);
}
/**
@@ -42,7 +49,9 @@ class BookRepo
*/
public function getAll($count = 10)
{
return $this->book->orderBy('name', 'asc')->take($count)->get();
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
if (!$count) return $bookQuery->get();
return $bookQuery->take($count)->get();
}
/**
@@ -52,7 +61,8 @@ class BookRepo
*/
public function getAllPaginated($count = 10)
{
return $this->book->orderBy('name', 'asc')->paginate($count);
return $this->bookQuery()
->orderBy('name', 'asc')->paginate($count);
}
@@ -63,7 +73,7 @@ class BookRepo
*/
public function getLatest($count = 10)
{
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
}
/**
@@ -92,11 +102,12 @@ class BookRepo
* Get a book by slug
* @param $slug
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->book->where('slug', '=', $slug)->first();
if ($book === null) abort(404);
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
@@ -107,7 +118,7 @@ class BookRepo
*/
public function exists($id)
{
return $this->book->where('id', '=', $id)->exists();
return $this->bookQuery()->where('id', '=', $id)->exists();
}
/**
@@ -117,17 +128,7 @@ class BookRepo
*/
public function newFromInput($input)
{
return $this->book->fill($input);
}
/**
* Count the amount of books that have a specific slug.
* @param $slug
* @return mixed
*/
public function countBySlug($slug)
{
return $this->book->where('slug', '=', $slug)->count();
return $this->book->newInstance($input);
}
/**
@@ -144,6 +145,7 @@ class BookRepo
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->restrictions()->delete();
$book->delete();
}
@@ -159,7 +161,7 @@ class BookRepo
}
/**
* @param string $slug
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
@@ -175,7 +177,7 @@ class BookRepo
/**
* Provides a suitable slug for the given book name.
* Ensures the returned slug is unique in the system.
* @param string $name
* @param string $name
* @param bool|false $currentId
* @return string
*/
@@ -196,34 +198,63 @@ class BookRepo
* Returns a sorted collection of Pages and Chapters.
* Loads the bookslug 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)
public function getChildren(Book $book, $filterDrafts = false)
{
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
$chapters = $book->chapters()->with('pages')->get();
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
if ($filterDrafts) {
$pageQuery = $pageQuery->where('draft', '=', false);
}
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
$this->restrictionService->enforcePageRestrictions($query, 'view');
if ($filterDrafts) $query->where('draft', '=', false);
}]);
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->merge($chapters);
$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;
});
}
});
return $children->sortBy('priority');
// 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)
public function getBySearch($term, $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
$terms = $this->prepareSearchTerms($term);
$books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) {
//highlight

View File

@@ -2,21 +2,19 @@
use Activity;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Chapter;
class ChapterRepo
class ChapterRepo extends EntityRepo
{
protected $chapter;
/**
* ChapterRepo constructor.
* @param $chapter
* Base query for getting chapters, Takes restrictions into account.
* @return mixed
*/
public function __construct(Chapter $chapter)
private function chapterQuery()
{
$this->chapter = $chapter;
return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
}
/**
@@ -26,7 +24,7 @@ class ChapterRepo
*/
public function idExists($id)
{
return $this->chapter->where('id', '=', $id)->count() > 0;
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
}
/**
@@ -36,7 +34,7 @@ class ChapterRepo
*/
public function getById($id)
{
return $this->chapter->findOrFail($id);
return $this->chapterQuery()->findOrFail($id);
}
/**
@@ -45,7 +43,7 @@ class ChapterRepo
*/
public function getAll()
{
return $this->chapter->all();
return $this->chapterQuery()->all();
}
/**
@@ -53,14 +51,30 @@ class ChapterRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) abort(404);
$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->restrictionService->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
@@ -85,6 +99,7 @@ class ChapterRepo
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->restrictions()->delete();
$chapter->delete();
}
@@ -121,16 +136,31 @@ class ChapterRepo
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 $term
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
$terms = $this->prepareSearchTerms($term);
$chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) {
//highlight

177
app/Repos/EntityRepo.php Normal file
View File

@@ -0,0 +1,177 @@
<?php namespace BookStack\Repos;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\Services\RestrictionService;
use BookStack\User;
class EntityRepo
{
/**
* @var Book $book
*/
public $book;
/**
* @var Chapter
*/
public $chapter;
/**
* @var Page
*/
public $page;
/**
* @var RestrictionService
*/
protected $restrictionService;
/**
* EntityService constructor.
*/
public function __construct()
{
$this->book = app(Book::class);
$this->chapter = app(Chapter::class);
$this->page = app(Page::class);
$this->restrictionService = app(RestrictionService::class);
}
/**
* Get the latest books added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->restrictionService->enforceBookRestrictions($this->book)
->orderBy('created_at', 'desc');
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->skip($page * $count)->take($count)->get();
}
/**
* Get the most recently updated books.
* @param $count
* @param int $page
* @return mixed
*/
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
{
return $this->restrictionService->enforceBookRestrictions($this->book)
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
}
/**
* Get the latest pages added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->restrictionService->enforcePageRestrictions($this->page)
->orderBy('created_at', 'desc')->where('draft', '=', false);
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->with('book')->skip($page * $count)->take($count)->get();
}
/**
* Get the latest chapters added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->restrictionService->enforceChapterRestrictions($this->chapter)
->orderBy('created_at', 'desc');
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->skip($page * $count)->take($count)->get();
}
/**
* Get the most recently updated pages.
* @param $count
* @param int $page
* @return mixed
*/
public function getRecentlyUpdatedPages($count = 20, $page = 0)
{
return $this->restrictionService->enforcePageRestrictions($this->page)
->where('draft', '=', false)
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
}
/**
* Get draft pages owned by the current user.
* @param int $count
* @param int $page
*/
public function getUserDraftPages($count = 20, $page = 0)
{
$user = auth()->user();
return $this->page->where('draft', '=', true)
->where('created_by', '=', $user->id)
->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get();
}
/**
* Updates entity restrictions from a request
* @param $request
* @param Entity $entity
*/
public function updateRestrictionsFromRequest($request, Entity $entity)
{
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
$entity->restrictions()->delete();
if ($request->has('restrictions')) {
foreach ($request->get('restrictions') as $roleId => $restrictions) {
foreach ($restrictions as $action => $value) {
$entity->restrictions()->create([
'role_id' => $roleId,
'action' => strtolower($action)
]);
}
}
}
$entity->save();
}
/**
* Prepare a string of search terms by turning
* it into an array of terms.
* Keeps quoted terms together.
* @param $termString
* @return array
*/
protected function prepareSearchTerms($termString)
{
preg_match_all('/"(.*?)"/', $termString, $matches);
if (count($matches[1]) > 0) {
$terms = $matches[1];
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
} else {
$terms = [];
}
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
return $terms;
}
}

View File

@@ -2,7 +2,9 @@
use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\RestrictionService;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -11,16 +13,22 @@ class ImageRepo
protected $image;
protected $imageService;
protected $restictionService;
protected $page;
/**
* ImageRepo constructor.
* @param Image $image
* @param Image $image
* @param ImageService $imageService
* @param RestrictionService $restrictionService
* @param Page $page
*/
public function __construct(Image $image, ImageService $imageService)
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
{
$this->image = $image;
$this->imageService = $imageService;
$this->restictionService = $restrictionService;
$this->page = $page;
}
@@ -34,23 +42,17 @@ class ImageRepo
return $this->image->findOrFail($id);
}
/**
* Gets a load images paginated, filtered by image type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* Execute a paginated query, returning in a standard format.
* Also runs the query through the restriction system.
* @param $query
* @param int $page
* @param int $pageSize
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
private function returnPaginated($query, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
}
$images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
@@ -65,15 +67,74 @@ class ImageRepo
];
}
/**
* Gets a load images paginated, filtered by image type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
{
$images = $this->image->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
}
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* Search for images by query, of a particular type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param string $searchTerm
* @return array
*/
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* Get gallery images with a particular filter criteria such as
* being within the current book or page.
* @param int $pagination
* @param int $pageSize
* @param $filter
* @param $pageId
* @return array
*/
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
{
$images = $this->image->where('type', '=', 'gallery');
$page = $this->page->findOrFail($pageId);
if ($filter === 'page') {
$images = $images->where('uploaded_to', '=', $page->id);
} elseif ($filter === 'book') {
$validPageIds = $page->book->pages->pluck('id')->toArray();
$images = $images->whereIn('uploaded_to', $validPageIds);
}
return $this->returnPaginated($images, $pagination, $pageSize);
}
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @return Image
*/
public function saveNew(UploadedFile $uploadFile, $type)
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type);
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$this->loadThumbs($image);
return $image;
}
@@ -123,9 +184,9 @@ class ImageRepo
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
* @param bool $keepRatio
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)

View File

@@ -1,59 +1,53 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use BookStack\Exceptions\NotFoundException;
use Carbon\Carbon;
use DOMDocument;
use Illuminate\Support\Str;
use BookStack\Page;
use BookStack\PageRevision;
class PageRepo
class PageRepo extends EntityRepo
{
protected $page;
protected $pageRevision;
/**
* PageRepo constructor.
* @param Page $page
* @param PageRevision $pageRevision
*/
public function __construct(Page $page, PageRevision $pageRevision)
public function __construct(PageRevision $pageRevision)
{
$this->page = $page;
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Check if a page id exists.
* @param $id
* @return bool
* Base query for getting pages, Takes restrictions into account.
* @param bool $allowDrafts
* @return mixed
*/
public function idExists($id)
private function pageQuery($allowDrafts = false)
{
return $this->page->where('page_id', '=', $id)->count() > 0;
$query = $this->restrictionService->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)
public function getById($id, $allowDrafts = false)
{
return $this->page->findOrFail($id);
}
/**
* Get all pages.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->page->all();
return $this->pageQuery($allowDrafts)->findOrFail($id);
}
/**
@@ -61,15 +55,37 @@ class PageRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) abort(404);
$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->restrictionService->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
*/
@@ -94,8 +110,8 @@ class PageRepo
* Save a new page into the system.
* Input validation must be done beforehand.
* @param array $input
* @param Book $book
* @param int $chapterId
* @param Book $book
* @param int $chapterId
* @return Page
*/
public function saveNew(array $input, Book $book, $chapterId = null)
@@ -114,6 +130,47 @@ class PageRepo
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);
$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();
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);
return $page;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
@@ -122,24 +179,23 @@ class PageRepo
*/
protected function formatHtml($htmlText)
{
if($htmlText == '') return $htmlText;
if ($htmlText == '') return $htmlText;
libxml_use_internal_errors(true);
$doc = new \DOMDocument();
$doc->loadHTML($htmlText);
$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
$lastId = false;
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
// Overwrite id if not a bookstack custom id
// 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) {
@@ -149,13 +205,18 @@ class PageRepo
}
// Create an unique id for the element
do {
$id = 'bkmrk-' . substr(uniqid(), -5);
} while ($id == $lastId);
$lastId = $id;
// 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', $id);
$idArray[] = $id;
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
@@ -171,14 +232,17 @@ class PageRepo
/**
* Gets pages by a search term.
* Highlights page content for showing in results.
* @param string $term
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
$terms = $this->prepareSearchTerms($term);
$pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
->paginate($count)->appends($paginationAppends);
// Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
@@ -215,7 +279,7 @@ class PageRepo
*/
public function searchForImage($imageString)
{
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
@@ -226,8 +290,8 @@ class PageRepo
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param Page $page
* @param int $book_id
* @param string $input
* @return Page
*/
@@ -238,13 +302,23 @@ class PageRepo
$this->saveRevision($page);
}
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
}
// Update with new details
$userId = auth()->user()->id;
$page->fill($input);
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->updated_by = auth()->user()->id;
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();
return $page;
}
@@ -275,9 +349,13 @@ class PageRepo
public function saveRevision(Page $page)
{
$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->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
@@ -287,6 +365,155 @@ class PageRepo
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
@@ -314,7 +541,7 @@ class PageRepo
/**
* 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 int $bookId
* @param Page $page
* @return Page
*/
@@ -355,8 +582,26 @@ class PageRepo
Activity::removeEntity($page);
$page->views()->delete();
$page->revisions()->delete();
$page->restrictions()->delete();
$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

@@ -0,0 +1,142 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\Permission;
use BookStack\Role;
use Setting;
class PermissionsRepo
{
protected $permission;
protected $role;
/**
* PermissionsRepo constructor.
* @param $permission
* @param $role
*/
public function __construct(Permission $permission, Role $role)
{
$this->permission = $permission;
$this->role = $role;
}
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
{
return $this->role->all();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
{
return $this->role->where('id', '!=', $role->id)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
{
return $this->role->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(str_random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
return $role;
}
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* @param $roleId
* @param $roleData
*/
public function updateRole($roleId, $roleData)
{
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
}
$role->fill($roleData);
$role->save();
}
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
public function assignRolePermissions(Role $role, $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray && count($permissionNameArray) > 0) {
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
}
$role->permissions()->sync($permissions);
}
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
// Prevent deleting admin role or default registration role.
if ($role->name === 'admin') {
throw new PermissionsException('The admin role 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.');
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}
$role->delete();
}
}

View File

@@ -1,23 +1,27 @@
<?php namespace BookStack\Repos;
use BookStack\Role;
use BookStack\User;
use Setting;
class UserRepo
{
protected $user;
protected $role;
protected $entityRepo;
/**
* UserRepo constructor.
* @param $user
* @param User $user
* @param Role $role
* @param EntityRepo $entityRepo
*/
public function __construct(User $user, Role $role)
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
{
$this->user = $user;
$this->role = $role;
$this->entityRepo = $entityRepo;
}
/**
@@ -38,6 +42,15 @@ class UserRepo
return $this->user->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsers()
{
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
* Creates a new user and attaches a role to them.
* @param array $data
@@ -47,6 +60,14 @@ class UserRepo
{
$user = $this->create($data);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
}
return $user;
}
@@ -56,8 +77,8 @@ class UserRepo
*/
public function attachDefaultRole($user)
{
$roleId = \Setting::get('registration-role');
if ($roleId === false) $roleId = $this->role->getDefault()->id;
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
$user->attachRoleId($roleId);
}
@@ -68,15 +89,10 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if ($user->role->name != 'admin') {
return false;
}
$adminRole = $this->role->where('name', '=', 'admin')->first();
if (count($adminRole->users) > 1) {
return false;
}
if (!$user->roles->pluck('name')->contains('admin')) return false;
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
return true;
}
@@ -87,10 +103,11 @@ class UserRepo
*/
public function create(array $data)
{
return $this->user->create([
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password'])
'password' => bcrypt($data['password']),
'email_confirmed' => false
]);
}
@@ -103,4 +120,62 @@ class UserRepo
$user->socialAccounts()->delete();
$user->delete();
}
/**
* Get the latest activity for a user.
* @param User $user
* @param int $count
* @param int $page
* @return array
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
}
/**
* Get the recently created content for this given user.
* @param User $user
* @param int $count
* @return mixed
*/
public function getRecentlyCreated(User $user, $count = 20)
{
return [
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
];
}
/**
* Get asset created counts for the give user.
* @param User $user
* @return array
*/
public function getAssetCounts(User $user)
{
return [
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
];
}
/**
* Get all the roles which can be given restricted access to
* other entities in the system.
* @return mixed
*/
public function getRestrictableRoles()
{
return $this->role->where('name', '!=', 'admin')->get();
}
}

21
app/Restriction.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class Restriction extends Model
{
protected $fillable = ['role_id', 'action'];
public $timestamps = false;
/**
* Get all this restriction's attached entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function restrictable()
{
return $this->morphTo();
}
}

View File

@@ -6,11 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* Sets the default role name for newly registed users.
* @var string
*/
protected static $default = 'viewer';
protected $fillable = ['display_name', 'description'];
/**
* The roles that belong to the role.
@@ -28,6 +25,15 @@ class Role extends Model
return $this->belongsToMany('BookStack\Permission');
}
/**
* Check if this role has a permission.
* @param $permission
*/
public function hasPermission($permission)
{
return $this->permissions->pluck('name')->contains($permission);
}
/**
* Add a permission to this role.
* @param Permission $permission
@@ -38,11 +44,21 @@ class Role extends Model
}
/**
* Get an instance of the default role.
* @return Role
* Detach a single permission from this role.
* @param Permission $permission
*/
public static function getDefault()
public function detachPermission(Permission $permission)
{
return static::where('name', '=', static::$default)->first();
$this->permissions()->detach($permission->id);
}
/**
* Get the role object for the specified role.
* @param $roleName
* @return mixed
*/
public static function getRole($roleName)
{
return static::where('name', '=', $roleName)->first();
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
use Illuminate\Support\Facades\Auth;
use BookStack\Activity;
use BookStack\Entity;
use Session;
@@ -9,14 +8,17 @@ class ActivityService
{
protected $activity;
protected $user;
protected $restrictionService;
/**
* ActivityService constructor.
* @param $activity
* @param Activity $activity
* @param RestrictionService $restrictionService
*/
public function __construct(Activity $activity)
public function __construct(Activity $activity, RestrictionService $restrictionService)
{
$this->activity = $activity;
$this->restrictionService = $restrictionService;
$this->user = auth()->user();
}
@@ -24,25 +26,26 @@ class ActivityService
* Add activity data to database.
* @param Entity $entity
* @param $activityKey
* @param int $bookId
* @param bool $extra
* @param int $bookId
* @param bool $extra
*/
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
{
$this->activity->user_id = $this->user->id;
$this->activity->book_id = $bookId;
$this->activity->key = strtolower($activityKey);
$activity = $this->activity->newInstance();
$activity->user_id = $this->user->id;
$activity->book_id = $bookId;
$activity->key = strtolower($activityKey);
if ($extra !== false) {
$this->activity->extra = $extra;
$activity->extra = $extra;
}
$entity->activity()->save($this->activity);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
}
/**
* Adds a activity history with a message & without binding to a entitiy.
* Adds a activity history with a message & without binding to a entity.
* @param $activityKey
* @param int $bookId
* @param int $bookId
* @param bool|false $extra
*/
public function addMessage($activityKey, $bookId = 0, $extra = false)
@@ -85,20 +88,22 @@ class ActivityService
*/
public function latest($count = 20, $page = 0)
{
$activityList = $this->activity->orderBy('created_at', 'desc')
->skip($count * $page)->take($count)->get();
$activityList = $this->restrictionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activityList);
}
/**
* Gets the latest activity for an entitiy, Filtering out similar
* Gets the latest activity for an entity, Filtering out similar
* items to prevent a message activity list.
* @param Entity $entity
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @return array
*/
function entityActivity($entity, $count = 20, $page = 0)
public function entityActivity($entity, $count = 20, $page = 0)
{
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
->skip($count * $page)->take($count)->get();
@@ -107,15 +112,31 @@ class ActivityService
}
/**
* Filters out similar acitivity.
* @param Activity[] $activity
* Get latest activity for a user, Filtering out similar
* items.
* @param $user
* @param int $count
* @param int $page
* @return array
*/
protected function filterSimilar($activity)
public function userActivity($user, $count = 20, $page = 0)
{
$activityList = $this->restrictionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activityList);
}
/**
* Filters out similar activity.
* @param Activity[] $activities
* @return array
*/
protected function filterSimilar($activities)
{
$newActivity = [];
$previousItem = false;
foreach ($activity as $activityItem) {
foreach ($activities as $activityItem) {
if ($previousItem === false) {
$previousItem = $activityItem;
$newActivity[] = $activityItem;

View File

@@ -45,7 +45,7 @@ class EmailConfirmationService
'token' => $token,
]);
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
$appName = \Setting::get('app-name', 'BookStack');
$appName = setting('app-name', 'BookStack');
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
});
}

View File

@@ -0,0 +1,115 @@
<?php namespace BookStack\Services;
use BookStack\Page;
class ExportService
{
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
*/
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
return $this->containHtml($pageHtml);
}
/**
* 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);
return $pdf->output();
}
/**
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @return mixed|string
*/
protected function containHtml($htmlContent)
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$pathString = public_path($srcString);
} else {
$pathString = $srcString;
}
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
}
}
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
}
}
}
// Replace any relative links with system domain
return $htmlContent;
}
/**
* Converts the page contents into simple plain text.
* 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;
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
$text = html_entity_decode($text);
// Add title
$text = $page->name . "\n\n" . $text;
return $text;
}
}

View File

@@ -1,7 +1,10 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@@ -38,14 +41,16 @@ class ImageService
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @return mixed
* @throws ImageUploadException
*/
public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
$imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath());
return $this->saveNew($imageName, $imageData, $type);
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
@@ -70,12 +75,14 @@ class ImageService
* @param string $imageName
* @param string $imageData
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
private function saveNew($imageName, $imageData, $type)
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
{
$storage = $this->getStorage();
$secureUploads = Setting::get('app-secure-images');
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
@@ -86,17 +93,27 @@ class ImageService
}
$fullPath = $imagePath . $imageName;
$storage->put($fullPath, $imageData);
try {
$storage->put($fullPath, $imageData);
} catch (Exception $e) {
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
}
$userId = auth()->user()->id;
$image = Image::forceCreate([
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
'url' => $this->getPublicUrl($fullPath),
'type' => $type,
'created_by' => $userId,
'updated_by' => $userId
]);
'uploaded_to' => $uploadedTo
];
if (auth()->user() && auth()->user()->id !== 0) {
$userId = auth()->user()->id;
$imageDetails['created_by'] = $userId;
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
return $image;
}
@@ -107,10 +124,12 @@ class ImageService
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
* @param bool $keepRatio
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws Exception
* @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
@@ -127,8 +146,16 @@ class ImageService
return $this->getPublicUrl($thumbFilePath);
}
// Otherwise create the thumbnail
$thumb = $this->imageTool->make($storage->get($image->path));
try {
$thumb = $this->imageTool->make($storage->get($image->path));
} 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.');
} else {
throw $e;
}
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
@@ -188,6 +215,7 @@ class ImageService
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->save();
return $image;
}
@@ -200,7 +228,7 @@ class ImageService
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = env('STORAGE_TYPE');
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
@@ -226,10 +254,10 @@ class ImageService
private function getPublicUrl($filePath)
{
if ($this->storageUrl === null) {
$storageUrl = env('STORAGE_URL');
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
if ($storageUrl == false && config('filesystems.default') === 's3') {
$storageDetails = config('filesystems.disks.s3');
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}

86
app/Services/Ldap.php Normal file
View File

@@ -0,0 +1,86 @@
<?php namespace BookStack\Services;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
* @package BookStack\Services
*/
class Ldap
{
/**
* Connect to a LDAP server.
* @param string $hostName
* @param int $port
* @return resource
*/
public function connect($hostName, $port)
{
return ldap_connect($hostName, $port);
}
/**
* Set the value of a LDAP option for the given connection.
* @param resource $ldapConnection
* @param int $option
* @param mixed $value
* @return bool
*/
public function setOption($ldapConnection, $option, $value)
{
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Search LDAP tree using the provided filter.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
/**
* Get entries from an ldap search result.
* @param resource $ldapConnection
* @param resource $ldapSearchResult
* @return array
*/
public function getEntries($ldapConnection, $ldapSearchResult)
{
return ldap_get_entries($ldapConnection, $ldapSearchResult);
}
/**
* Search and get entries immediately.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
return $this->getEntries($ldapConnection, $search);
}
/**
* Bind to LDAP directory.
* @param resource $ldapConnection
* @param string $bindRdn
* @param string $bindPassword
* @return bool
*/
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}

View File

@@ -0,0 +1,148 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
/**
* Class LdapService
* Handles any app-specific LDAP tasks.
* @package BookStack\Services
*/
class LdapService
{
protected $ldap;
protected $ldapConnection;
protected $config;
/**
* LdapService constructor.
* @param Ldap $ldap
*/
public function __construct(Ldap $ldap)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
* @param $userName
* @return array|null
* @throws LdapException
*/
public function getUserDetails($userName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// 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']);
if ($users['count'] === 0) return null;
$user = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
];
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
public function validateUserCredentials(Authenticatable $user, $username, $password)
{
$ldapUser = $this->getUserDetails($username);
if ($ldapUser === null) return false;
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
$ldapConnection = $this->getConnection();
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
} catch (\ErrorException $e) {
$ldapBind = false;
}
return $ldapBind;
}
/**
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
* @param $connection
* @throws LdapException
*/
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
$ldapBind = $this->ldap->bind($connection);
} else {
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
}
/**
* Get the connection to the LDAP server.
* Creates a new connection if one does not exist.
* @return resource
* @throws LdapException
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) return $this->ldapConnection;
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException('LDAP PHP extension not installed');
}
// Get port from server string if specified.
$ldapServer = explode(':', $this->config['server']);
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
if ($ldapConnection === false) {
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
}
// Set any required options
if ($this->config['version']) {
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
* @param array $attrs
* @return string
*/
protected function buildFilter($filterString, array $attrs)
{
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $attrText;
}
return strtr($filterString, $newAttrs);
}
}

View File

@@ -0,0 +1,326 @@
<?php namespace BookStack\Services;
use BookStack\Entity;
class RestrictionService
{
protected $userRoles;
protected $isAdmin;
protected $currentAction;
protected $currentUser;
/**
* RestrictionService constructor.
*/
public function __construct()
{
$this->currentUser = auth()->user();
$this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
$this->isAdmin = $this->currentUser ? $this->currentUser->hasRole('admin') : false;
}
/**
* Checks if an entity has a restriction set upon it.
* @param Entity $entity
* @param $action
* @return bool
*/
public function checkIfEntityRestricted(Entity $entity, $action)
{
if ($this->isAdmin) return true;
$this->currentAction = $action;
$baseQuery = $entity->where('id', '=', $entity->id);
if ($entity->isA('page')) {
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
} elseif ($entity->isA('chapter')) {
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
} elseif ($entity->isA('book')) {
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
}
return false;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/**
* Add restrictions for a page query
* @param $query
* @param string $action
* @return mixed
*/
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);
});
}
});
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->pageRestrictionQuery($query);
}
/**
* The base query for restricting pages.
* @param $query
* @return mixed
*/
private function pageRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
->where(function ($query) {
$query->where(function ($query) {
$query->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id')
->where('restricted', '=', false);
})->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->where('restricted', '=', false);
})->where('restricted', '=', false);
})->orWhere(function ($query) {
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->where('restricted', '=', false);
});
});
})
// Page unrestricted, Has no chapter & book has accepted restrictions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id');
}, 'and', true)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
})
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// Page unrestricted, Has a chapter with accepted permissions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id')
->where('restricted', '=', true)
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
});
});
})
// Page has accepted permissions
->orWhereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'pages', 'Page');
});
});
}
/**
* Add on permission restrictions to a chapter query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceChapterRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->chapterRestrictionQuery($query);
}
/**
* The base query for restricting chapters.
* @param $query
* @return mixed
*/
private function chapterRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
// Book & chapter unrestricted
->where(function ($query) {
$query->where('restricted', '=', false)->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=chapters.book_id')
->where('restricted', '=', false);
});
})
// Chapter unrestricted & book has accepted restrictions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=chapters.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// Chapter has accepted permissions
->orWhereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
});
});
}
/**
* Add restrictions to a book query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceBookRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->bookRestrictionQuery($query);
}
/**
* The base query for restricting books.
* @param $query
* @return mixed
*/
private function bookRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
->where('restricted', '=', false)
->orWhere(function ($query) {
$query->where('restricted', '=', true)->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
});
}
/**
* Filter items that have entities set a a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{
if ($this->isAdmin) return $query;
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
return $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->pageRestrictionQuery($query);
});
});
})->orWhere(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->bookRestrictionQuery($query);
});
});
})->orWhere(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->chapterRestrictionQuery($query);
});
});
});
});
}
/**
* Filters pages that are a direct relation to another item.
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin) return $query;
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
return $query->where(function ($query) use (&$tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->pageRestrictionQuery($query);
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
});
}
/**
* The query to check the restrictions on an entity.
* @param $query
* @param $tableName
* @param $modelName
*/
private function checkRestrictionsQuery($query, $tableName, $modelName)
{
$query->select('*')->from('restrictions')
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
->where('restrictions.action', '=', $this->currentAction)
->whereIn('restrictions.role_id', $this->userRoles);
}
}

View File

@@ -38,33 +38,52 @@ class SettingService
*/
public function get($key, $default = false)
{
$value = $this->getValueFromStore($key, $default);
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
* @param $key
* @param $default
* @return mixed
*/
protected function getValueFromStore($key, $default)
{
// Check for an overriding value
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
// Check the cache
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
if($settingObject !== null) {
if ($settingObject !== null) {
$value = $settingObject->value;
$this->cache->forever($cacheKey, $value);
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;
}
/**
* Clear an item from the cache completely.
* @param $key
*/
protected function clearFromCache($key)
{
$cacheKey = $this->cachePrefix . $key;
@@ -136,9 +155,23 @@ class SettingService
* @param $key
* @return mixed
*/
private function getSettingObjectByKey($key)
protected function getSettingObjectByKey($key)
{
return $this->setting->where('setting_key', '=', $key)->first();
}
/**
* Returns an override value for a setting based on certain app conditions.
* Used where certain configuration options overrule others.
* Returns null if no override value is available.
* @param $key
* @return bool|null
*/
protected function getOverrideValue($key)
{
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
return null;
}
}

View File

@@ -76,9 +76,9 @@ class SocialAuthService
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
}
if($this->userRepo->getByEmail($socialUser->getEmail())) {
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('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
}
return $socialUser;
@@ -129,13 +129,13 @@ class SocialAuthService
// 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 buy another user.');
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
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';
if (\Setting::get('registration-enabled')) {
if (setting('registration-enabled')) {
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
}
throw new SocialSignInException($message . '.', '/login');
@@ -172,9 +172,10 @@ class SocialAuthService
*/
private function checkDriverConfigured($driver)
{
$upperName = strtoupper($driver);
$config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
return (!in_array(false, $config) && !in_array(null, $config));
$lowerName = strtolower($driver);
$configPrefix = 'services.' . $lowerName . '.';
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
return !in_array(false, $config) && !in_array(null, $config);
}
/**
@@ -193,7 +194,7 @@ class SocialAuthService
}
/**
* @param string $socialDriver
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
* @return SocialAccount
*/

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
use BookStack\Entity;
use BookStack\View;
@@ -9,15 +8,18 @@ class ViewService
protected $view;
protected $user;
protected $restrictionService;
/**
* ViewService constructor.
* @param $view
* @param View $view
* @param RestrictionService $restrictionService
*/
public function __construct(View $view)
public function __construct(View $view, RestrictionService $restrictionService)
{
$this->view = $view;
$this->user = auth()->user();
$this->restrictionService = $restrictionService;
}
/**
@@ -27,7 +29,7 @@ class ViewService
*/
public function add(Entity $entity)
{
if($this->user === null) return 0;
if ($this->user === null) return 0;
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
// Add view if model exists
if ($view) {
@@ -44,52 +46,47 @@ class ViewService
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @param bool|false $filterModel
*/
public function getPopular($count = 10, $page = 0, $filterModel = false)
{
$skipCount = $count * $page;
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}
/**
* Get all recently viewed entities for the current user.
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @param Entity|bool $filterModel
* @return mixed
*/
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
if($this->user === null) return collect();
$skipCount = $count * $page;
$query = $this->view->where('user_id', '=', auth()->user()->id);
if ($this->user === null) return collect();
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$query = $this->restrictionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', auth()->user()->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');
return $viewables;
}
/**
* Reset all view counts by deleting all views.
*/
@@ -98,5 +95,4 @@ class ViewService
$this->view->truncate();
}
}

View File

@@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'password', 'image_id'];
protected $fillable = ['name', 'email', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
@@ -50,10 +47,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
]);
}
/**
* Permissions and roles
*/
/**
* The roles that belong to the user.
*/
@@ -62,21 +55,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->belongsToMany('BookStack\Role');
}
public function getRoleAttribute()
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasRole($role)
{
return $this->roles()->with('permissions')->first();
return $this->roles->pluck('name')->contains($role);
}
/**
* Loads the user's permissions from thier role.
* Get all permissions belonging to a the current user.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
private function loadPermissions()
public function permissions($cache = true)
{
if (isset($this->permissions)) return;
if(isset($this->permissions) && $cache) return $this->permissions;
$this->load('roles.permissions');
$permissions = $this->roles[0]->permissions;
$permissionsArray = $permissions->pluck('name')->all();
$this->permissions = $permissionsArray;
$permissions = $this->roles->map(function($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
}
/**
@@ -86,11 +88,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if ($this->email == 'guest') {
return false;
}
$this->loadPermissions();
return array_search($permissionName, $this->permissions) !== false;
if ($this->email === 'guest') return false;
return $this->permissions()->pluck('name')->contains($permissionName);
}
/**
@@ -108,12 +107,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function attachRoleId($id)
{
$this->roles()->sync([$id]);
$this->roles()->attach($id);
}
/**
* Get the social account associated with this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function socialAccounts()
@@ -138,8 +136,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Returns the user's avatar,
* Uses Gravatar as the avatar service.
*
* @param int $size
* @return string
*/
@@ -164,6 +160,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getEditUrl()
{
return '/users/' . $this->id;
return '/settings/users/' . $this->id;
}
/**
* Get a shortened version of the user's name.
* @param int $chars
* @return string
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) return $this->name;
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) return $splitName[0];
return '';
}
}

View File

@@ -1,10 +1,10 @@
<?php
if (! function_exists('versioned_asset')) {
if (!function_exists('versioned_asset')) {
/**
* Get the path to a versioned file.
*
* @param string $file
* @param string $file
* @return string
*
* @throws \InvalidArgumentException
@@ -27,4 +27,48 @@ if (! function_exists('versioned_asset')) {
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
}
}
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the permissions are checked against
* that particular item.
* @param $permission
* @param \BookStack\Ownable $ownable
* @return mixed
*/
function userCan($permission, \BookStack\Ownable $ownable = null)
{
if (!auth()->check()) return false;
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
}
// Check permission on ownable item
$permissionBaseName = strtolower($permission) . '-';
$hasPermission = false;
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
// Check restrictions on the entity
$restrictionService = app('BookStack\Services\RestrictionService');
$explodedPermission = explode('-', $permission);
$action = end($explodedPermission);
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
$restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
}
/**
* Helper to access system settings.
* @param $key
* @param bool $default
* @return mixed
*/
function setting($key, $default = false)
{
$settingService = app('BookStack\Services\SettingService');
return $settingService->get($key, $default);
}

View File

@@ -6,18 +6,22 @@
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.1.*",
"laravel/framework": "5.2.*",
"intervention/image": "^2.3",
"laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.0",
"league/flysystem-aws-s3-v3": "^1.0"
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "0.6.*",
"predis/predis": "^1.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1"
"phpspec/phpspec": "~2.1",
"symfony/dom-crawler": "~3.0",
"symfony/css-selector": "~3.0"
},
"autoload": {
"classmap": [

1337
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,11 @@
return [
'env' => env('APP_ENV', 'production'),
'editor' => env('APP_EDITOR', 'html'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
@@ -113,13 +118,11 @@ return [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Routing\ControllerServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
@@ -142,6 +145,7 @@ return [
* Third Party
*/
Intervention\Image\ImageServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,
@@ -149,6 +153,7 @@ return [
/*
* Application Service Providers...
*/
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
@@ -208,6 +213,7 @@ return [
*/
'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
/**

View File

@@ -2,66 +2,109 @@
return [
'method' => env('AUTH_METHOD', 'standard'),
/*
|--------------------------------------------------------------------------
| Default Authentication Driver
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the authentication driver that will be utilized.
| This driver manages the retrieval and authentication of the users
| attempting to get access to protected areas of your application.
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'driver' => 'eloquent',
'providers' => [
'users' => [
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
'model' => BookStack\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Authentication Model
|--------------------------------------------------------------------------
|
| When using the "Eloquent" authentication driver, we need to know which
| Eloquent model should be used to retrieve your users. Of course, it
| is often just the "User" model but you may use whatever you like.
|
*/
'model' => BookStack\User::class,
/*
|--------------------------------------------------------------------------
| Authentication Table
|--------------------------------------------------------------------------
|
| When using the "Database" authentication driver, we need to know which
| table should be used to retrieve your users. We have chosen a basic
| default value but you may easily change it to any table you like.
|
*/
'table' => 'users',
/*
|--------------------------------------------------------------------------
| Password Reset Settings
| Resetting Passwords
|--------------------------------------------------------------------------
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You can also set the name of the
| that is your password reset e-mail. You may also set the name of the
| table that maintains all of the reset tokens for your application.
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
'passwords' => [
'users' => [
'provider' => 'users',
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
],
];
];

View File

@@ -1,5 +1,17 @@
<?php
// MEMCACHED - Split out configuration into an array
if (env('CACHE_DRIVER') === 'memcached') {
$memcachedServerKeys = ['host', 'port', 'weight'];
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
foreach ($memcachedServers as $index => $memcachedServer) {
$memcachedServerDetails = explode(':', $memcachedServer);
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
}
}
return [
/*
@@ -49,11 +61,7 @@ return [
'memcached' => [
'driver' => 'memcached',
'servers' => [
[
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
],
],
'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
],
'redis' => [
@@ -74,6 +82,6 @@ return [
|
*/
'prefix' => 'laravel',
'prefix' => env('CACHE_PREFIX', 'bookstack'),
];

View File

@@ -1,5 +1,21 @@
<?php
// REDIS - Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisServerKeys = ['host', 'port', 'database'];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
}
}
return [
/*
@@ -123,16 +139,6 @@ return [
|
*/
'redis' => [
'cluster' => false,
'default' => [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
],
],
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
];

266
config/dompdf.php Normal file
View File

@@ -0,0 +1,266 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
/**
* The location of the DOMPDF font directory
*
* The location of the directory where DOMPDF will store fonts and font metrics
* Note: This directory must exist and be writable by the webserver process.
* *Please note the trailing slash.*
*
* Notes regarding fonts:
* Additional .afm font metrics can be added by executing load_font.php from command line.
*
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
* increase file size unless font subsetting is enabled. Before embedding a font please
* review your rights under the font license.
*
* Any font specification in the source HTML is translated to the closest font available
* in the font directory.
*
* The pdf standard "Base 14 fonts" are:
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
*
* This directory contains the cached font metrics for the fonts used by DOMPDF.
* This directory can be the same as DOMPDF_FONT_DIR
*
* Note: This directory must exist and be writable by the webserver process.
*/
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
/**
* The location of a temporary directory.
*
* The directory specified must be writeable by the webserver process.
* The temporary directory is required to download remote images and when
* using the PFDLib back end.
*/
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
/**
* ==== IMPORTANT ====
*
* dompdf's "chroot": Prevents dompdf from accessing system files or other
* files on the webserver. All local files opened by dompdf must be in a
* subdirectory of this directory. DO NOT set it to '/' since this could
* allow an attacker to use dompdf to read any files on the server. This
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
"DOMPDF_CHROOT" => realpath(base_path()),
/**
* Whether to use Unicode fonts or not.
*
* When set to true the PDF backend must be set to "CPDF" and fonts must be
* loaded via load_font.php.
*
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
* document must be present in your fonts, however.
*/
"DOMPDF_UNICODE_ENABLED" => true,
/**
* Whether to enable font subsetting or not.
*/
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
* Canvas_Factory} ultimately determines which rendering class to instantiate
* based on this setting.
*
* Both PDFLib & CPDF rendering backends provide sufficient rendering
* capabilities for dompdf, however additional features (e.g. object,
* image and font support, etc.) differ between backends. Please see
* {@link PDFLib_Adapter} for more information on the PDFLib backend
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
* on CPDF. Also see the documentation for each backend at the links
* below.
*
* The GD rendering backend is a little different than PDFLib and
* CPDF. Several features of CPDF and PDFLib are not supported or do
* not make any sense when creating image files. For example,
* multiple pages are not supported, nor are PDF 'objects'. Have a
* look at {@link GD_Adapter} for more information. GD support is
* experimental, so use it at your own risk.
*
* @link http://www.pdflib.com
* @link http://www.ros.co.nz/pdf
* @link http://www.php.net/image
*/
"DOMPDF_PDF_BACKEND" => "CPDF",
/**
* PDFlib license key
*
* If you are using a licensed, commercial version of PDFlib, specify
* your license key here. If you are using PDFlib-Lite or are evaluating
* the commercial version of PDFlib, comment out this setting.
*
* @link http://www.pdflib.com
*
* If pdflib present in web server and auto or selected explicitely above,
* a real license code must exist!
*/
//"DOMPDF_PDFLIB_LICENSE" => "your license key here",
/**
* html target media view which should be rendered into pdf.
* List of types and parsing rules for future extensions:
* http://www.w3.org/TR/REC-html40/types.html
* screen, tty, tv, projection, handheld, print, braille, aural, all
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
* Note, even though the generated pdf file is intended for print output,
* 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",
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
* @var string
*/
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explictly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* Rendering resolution of various browsers in px per inch:
* Windows Firefox and Internet Explorer:
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
* Linux Firefox:
* about:config *resolution: Default:96
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
*
* Take care about extra font/image zoom factor of browser.
*
* In images, <img> size in pixel attribute, img css style, are overriding
* the real image dimension in px for rendering.
*
* @var int
*/
"DOMPDF_DPI" => 96,
/**
* Enable inline PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate
* inline PHP contained within <script type="text/php"> ... </script> tags.
*
* Enabling this for documents you do not trust (e.g. arbitrary remote html
* pages) is a security risk. Set this option to false if you wish to process
* untrusted documents.
*
* @var bool
*/
"DOMPDF_ENABLE_PHP" => false,
/**
* Enable inline Javascript
*
* If this setting is set to true then DOMPDF will automatically insert
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
*
* @var bool
*/
"DOMPDF_ENABLE_JAVASCRIPT" => true,
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
* This is required for part of test case www/test/image_variants.html through www/examples.php
*
* Attention!
* This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
* allowing remote access to dompdf.php or on allowing remote html code to be passed to
* $dompdf = new DOMPDF(, $dompdf->load_html(...,
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* @var bool
*/
"DOMPDF_ENABLE_REMOTE" => true,
/**
* A ratio applied to the fonts height to be more like browsers' line height
*/
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
/**
* Enable CSS float
*
* Allows people to disabled CSS float support
* @var bool
*/
"DOMPDF_ENABLE_CSS_FLOAT" => true,
/**
* Use the more-than-experimental HTML5 Lib parser
*/
"DOMPDF_ENABLE_HTML5PARSER" => true,
),
);

View File

@@ -15,7 +15,18 @@ return [
|
*/
'default' => 'local',
'default' => env('STORAGE_TYPE', 'local'),
/*
|--------------------------------------------------------------------------
| Storage URL
|--------------------------------------------------------------------------
|
| This is the url to where the storage is located for when using an external
| file storage service, such as s3, to store publicly accessible assets.
|
*/
'url' => env('STORAGE_URL', false),
/*
|--------------------------------------------------------------------------

View File

@@ -13,6 +13,8 @@ return [
| to have a conventional place to find your various credentials.
|
*/
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
'callback_url' => env('APP_URL', false),
'mailgun' => [
'domain' => '',
@@ -47,4 +49,13 @@ return [
'redirect' => env('APP_URL') . '/login/service/google/callback',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),
'dn' => env('LDAP_DN', false),
'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)
]
];

View File

@@ -0,0 +1,10 @@
<?php
/**
* The defaults for the system settings that are saved in the database.
*/
return [
'app-editor' => 'wysiwyg'
];

View File

@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
'email_confirmed' => 1
];
});
@@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) {
'text' => strip_tags($html)
];
});
$factory->define(BookStack\Role::class, function ($faker) {
return [
'display_name' => $faker->sentence(3),
'description' => $faker->sentence(10)
];
});

View File

@@ -18,13 +18,13 @@ class CreateUsersTable extends Migration
$table->string('email')->unique();
$table->string('password', 60);
$table->rememberToken();
$table->timestamps();
$table->nullableTimestamps();
});
\BookStack\User::create([
\BookStack\User::forceCreate([
'name' => 'Admin',
'email' => 'admin@admin.com',
'password' => \Illuminate\Support\Facades\Hash::make('password')
'password' => bcrypt('password')
]);
}

View File

@@ -17,7 +17,7 @@ class CreateBooksTable extends Migration
$table->string('name');
$table->string('slug')->indexed();
$table->text('description');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -21,7 +21,7 @@ class CreatePagesTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
$table->increments('id');
$table->string('name');
$table->string('url');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration
$table->text('name');
$table->text('description');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('created_by');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
$table->integer('user_id');
$table->integer('entity_id');
$table->string('entity_type');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
$table->nullableTimestamps();
});
// Create table for associating roles to users (Many-to-Many)
@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
$table->nullableTimestamps();
});
// Create table for associating permissions to roles (Many-to-Many)

View File

@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration
Schema::create('settings', function (Blueprint $table) {
$table->string('setting_key')->primary()->indexed();
$table->text('value');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
$table->string('driver')->index();
$table->string('driver_id');
$table->string('avatar');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
$table->increments('id');
$table->integer('user_id')->index();
$table->string('token')->index();
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
$table->integer('viewable_id');
$table->string('viewable_type');
$table->integer('views');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddExternalAuthToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('external_auth_id')->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('external_auth_id');
});
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSlugToRevisions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->string('slug');
$table->index('slug');
$table->string('book_slug');
$table->index('book_slug');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('slug');
$table->dropColumn('book_slug');
});
}
}

View File

@@ -0,0 +1,99 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Get roles with permissions we need to change
$adminRole = \BookStack\Role::getRole('admin');
$editorRole = \BookStack\Role::getRole('editor');
// Delete old permissions
$permissions = \BookStack\Permission::all();
$permissions->each(function ($permission) {
$permission->delete();
});
// Create & attach new admin permissions
$permissionsToCreate = [
'settings-manage' => 'Manage Settings',
'users-manage' => 'Manage Users',
'user-roles-manage' => 'Manage Roles & Permissions',
'restrictions-manage-all' => 'Manage All Entity Restrictions',
'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
];
foreach ($permissionsToCreate as $name => $displayName) {
$newPermission = new \BookStack\Permission();
$newPermission->name = $name;
$newPermission->display_name = $displayName;
$newPermission->save();
$adminRole->attachPermission($newPermission);
}
// Create & attach new entity permissions
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermission = new \BookStack\Permission();
$newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
$newPermission->display_name = $op . ' ' . $entity . 's';
$newPermission->save();
$adminRole->attachPermission($newPermission);
if ($editorRole !== null) $editorRole->attachPermission($newPermission);
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Get roles with permissions we need to change
$adminRole = \BookStack\Role::getRole('admin');
// Delete old permissions
$permissions = \BookStack\Permission::all();
$permissions->each(function ($permission) {
$permission->delete();
});
// Create default CRUD permissions and allocate to admins and editors
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermission = new \BookStack\Permission();
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
$newPermission->display_name = $op . ' ' . $entity . 's';
$newPermission->save();
$adminRole->attachPermission($newPermission);
}
}
// Create admin permissions
$entities = ['Settings', 'User'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermission = new \BookStack\Permission();
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
$newPermission->display_name = $op . ' ' . $entity;
$newPermission->save();
$adminRole->attachPermission($newPermission);
}
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEntityAccessControls extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('images', function (Blueprint $table) {
$table->integer('uploaded_to')->default(0);
$table->index('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::create('restrictions', function(Blueprint $table) {
$table->increments('id');
$table->integer('restrictable_id');
$table->string('restrictable_type');
$table->integer('role_id');
$table->string('action');
$table->index('role_id');
$table->index('action');
$table->index(['restrictable_id', 'restrictable_type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('images', function (Blueprint $table) {
$table->dropColumn('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::drop('restrictions');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPageRevisionTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->string('type')->default('version');
$table->index('type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('type');
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPageDrafts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function(Blueprint $table) {
$table->boolean('draft')->default(false);
$table->index('draft');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('draft');
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMarkdownSupport extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
$table->longText('markdown')->default('');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->longText('markdown')->default('');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('markdown');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('markdown');
});
}
}

View File

@@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$role = \BookStack\Role::where('name', '=', 'admin')->first();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);

View File

@@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
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']);
});

View File

@@ -12,6 +12,8 @@
"bootstrap-sass": "^3.0.0",
"dropzone": "^4.0.1",
"laravel-elixir": "^3.4.0",
"marked": "^0.3.5",
"moment": "^2.12.0",
"zeroclipboard": "^2.2.0"
}
}

View File

@@ -21,11 +21,18 @@
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="mysql_testing"/>
<env name="MAIL_PRETEND" value="true"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
<env name="MAIL_DRIVER" value="log"/>
<env name="AUTH_METHOD" value="standard"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
<env name="LDAP_VERSION" value="3"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
</php>
</phpunit>

2
public/build/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

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

1
public/css/export-styles.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.faded-small,.print-hidden,header{display:none}@font-face{font-family:Roboto;src:url(/fonts/roboto-bold-webfont.eot);src:url(/fonts/roboto-bold-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-bold-webfont.woff2) format("woff2"),url(/fonts/roboto-bold-webfont.woff) format("woff"),url(/fonts/roboto-bold-webfont.ttf) format("truetype"),url(/fonts/roboto-bold-webfont.svg#robotobold) format("svg");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-bolditalic-webfont.eot);src:url(/fonts/roboto-bolditalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-bolditalic-webfont.woff2) format("woff2"),url(/fonts/roboto-bolditalic-webfont.woff) format("woff"),url(/fonts/roboto-bolditalic-webfont.ttf) format("truetype"),url(/fonts/roboto-bolditalic-webfont.svg#robotobold_italic) format("svg");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-italic-webfont.eot);src:url(/fonts/roboto-italic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-italic-webfont.woff2) format("woff2"),url(/fonts/roboto-italic-webfont.woff) format("woff"),url(/fonts/roboto-italic-webfont.ttf) format("truetype"),url(/fonts/roboto-italic-webfont.svg#robotoitalic) format("svg");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-light-webfont.eot);src:url(/fonts/roboto-light-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-light-webfont.woff2) format("woff2"),url(/fonts/roboto-light-webfont.woff) format("woff"),url(/fonts/roboto-light-webfont.ttf) format("truetype"),url(/fonts/roboto-light-webfont.svg#robotolight) format("svg");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-lightitalic-webfont.eot);src:url(/fonts/roboto-lightitalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-lightitalic-webfont.woff2) format("woff2"),url(/fonts/roboto-lightitalic-webfont.woff) format("woff"),url(/fonts/roboto-lightitalic-webfont.ttf) format("truetype"),url(/fonts/roboto-lightitalic-webfont.svg#robotolight_italic) format("svg");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-medium-webfont.eot);src:url(/fonts/roboto-medium-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-medium-webfont.woff2) format("woff2"),url(/fonts/roboto-medium-webfont.woff) format("woff"),url(/fonts/roboto-medium-webfont.ttf) format("truetype"),url(/fonts/roboto-medium-webfont.svg#robotomedium) format("svg");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-mediumitalic-webfont.eot);src:url(/fonts/roboto-mediumitalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-mediumitalic-webfont.woff2) format("woff2"),url(/fonts/roboto-mediumitalic-webfont.woff) format("woff"),url(/fonts/roboto-mediumitalic-webfont.ttf) format("truetype"),url(/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic) format("svg");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-regular-webfont.eot);src:url(/fonts/roboto-regular-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-regular-webfont.woff2) format("woff2"),url(/fonts/roboto-regular-webfont.woff) format("woff"),url(/fonts/roboto-regular-webfont.ttf) format("truetype"),url(/fonts/roboto-regular-webfont.svg#robotoregular) format("svg");font-weight:400;font-style:normal}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}
.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

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

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